Compare commits

...

12 Commits

Author SHA1 Message Date
Miguel b74db36cf9 Version Funcionante 2025-06-19 20:55:40 +02:00
Miguel 26bc892243 Actualización de directorios de trabajo y logs en el script de obtención de IO de TIA Portal
- Se modificaron los directorios de trabajo en `script_config.json` y `work_dir.json` para apuntar a la nueva ubicación de los archivos del proyecto 98050.
- Se actualizaron los logs de ejecución en `log_x1.txt` y `log_x4.txt` para reflejar las nuevas fechas, duraciones y resultados de los procesos de exportación.
- Se registraron errores en `log_x4.txt` relacionados con el acceso al proyecto, indicando que ya estaba abierto por otro usuario.
2025-06-19 19:07:49 +02:00
Miguel 99e3628955 Eliminación de archivos de ejemplo en el convertidor LAD de TwinCAT. Se han eliminado múltiples archivos .EXP que ya no son necesarios, optimizando así la estructura del proyecto y reduciendo la confusión en la gestión de ejemplos. Se mantiene la funcionalidad principal del convertidor en `x1_lad_converter.py` para la conversión de archivos .EXP a .SCL. 2025-06-19 18:39:04 +02:00
Miguel f57d0f21dc Actualización de directorios de trabajo y mejora en la gestión de logs
- Se modificaron los directorios de trabajo en `script_config.json` y `work_dir.json` para apuntar a la nueva ubicación de los archivos relacionados con el proyecto SIDEL.
- Se actualizaron los logs de ejecución en `log_x1.txt` y `log_x4.txt` para reflejar las nuevas fechas, duraciones y resultados de los procesos de exportación.
- Se implementó una nueva función de limpieza en `x0_main.py` para eliminar artefactos generados durante la ejecución de los scripts, mejorando la gestión de archivos temporales.
- Se realizaron ajustes en la interfaz de usuario para mejorar la experiencia al seleccionar y confirmar directorios de trabajo.
2025-06-19 18:05:47 +02:00
Miguel 5da7dcad06 Mejora en el convertidor LAD de TwinCAT con integración de SymPy para optimización de expresiones lógicas. Se añadieron nuevas funcionalidades para el manejo de variables y ACTIONs, así como mejoras en la estructura del código SCL generado. 2025-06-19 15:13:24 +02:00
Miguel 205e1f4c8d Mejorado de la conversion LAD de Twincat 2025-06-19 14:45:27 +02:00
Miguel c597eaa28f Primera Version TwinCat Converter 2025-06-19 14:30:15 +02:00
Miguel 95eb1bc62f - Se mejoró la lógica de procesamiento de tablas en `x5_md_to_excel.py`, permitiendo la lectura y combinación de múltiples tablas en un DataFrame.
- Se implementaron mensajes de depuración adicionales para facilitar el seguimiento de errores y el estado de los procesos.
2025-06-19 00:09:05 +02:00
Miguel e3eb2fb9e5 Mejoras en el Launcher C# y gestión de proyectos
- Se añadió la funcionalidad de limpieza del estado previo en el gestor de lanzadores C#.
- Se implementó un método para reiniciar completamente el launcher, asegurando una inicialización adecuada.
- Se mejoró la gestión de argumentos para ejecutables, permitiendo la selección y ejecución con argumentos predefinidos.
- Se optimizó la lógica de inicialización y se añadieron mensajes de depuración para facilitar el seguimiento de errores.
- Se realizaron ajustes en la interfaz para mejorar la experiencia del usuario al gestionar proyectos y ejecutables.
2025-06-18 02:44:36 +02:00
Miguel 5be80138c5 Implementación de mejoras en el Launcher C# y gestión de proyectos
- Se completó la implementación del editor de proyectos C#, permitiendo agregar, editar y eliminar proyectos con un formulario avanzado.
- Se mejoró la gestión de ejecutables C#, incluyendo la capacidad de ejecutar, obtener metadatos y argumentos predefinidos.
- Se añadieron nuevas rutas API para gestionar proyectos y ejecutables C#, mejorando la funcionalidad y la experiencia del usuario.
- Se actualizaron los logs para reflejar la ejecución de procesos y se implementaron notificaciones para el manejo de errores.
- Se mejoró la interfaz de usuario con nuevos modales para la gestión de proyectos y ejecutables, facilitando la interacción.
2025-06-18 01:58:03 +02:00
Miguel 7ab11a94ce Implementación del Launcher C# y mejoras en la interfaz de usuario
- Se añadió un nuevo launcher para proyectos C# que permite gestionar, ejecutar y categorizar aplicaciones compiladas.
- Se implementaron tres pestañas en la interfaz: "Scripts (Config)", "Launcher GUI (Python)" y "Launcher C#", mejorando la organización y accesibilidad.
- Se actualizaron los archivos de configuración y se mejoró la lógica de inicialización para soportar el nuevo sistema de C#.
- Se realizaron ajustes en la interfaz para incluir un panel de favoritos y un sistema de gestión de procesos en ejecución para C#.
- Se mejoró la documentación en `adicion_launcher4GUI.md` para reflejar las nuevas funcionalidades y estructura del proyecto.
2025-06-17 17:48:13 +02:00
Miguel bf30b2db52 Actualización de logs y mejora en la gestión de excepciones en el script de exportación
- Se actualizaron los registros de ejecución en `log_x4.txt` para reflejar nuevas fechas y duraciones de los procesos.
- Se implementó una nueva clase `PortalDisposedException` para manejar excepciones relacionadas con el cierre inesperado de TIA Portal.
- Se mejoró la lógica de re-apertura del portal en `x4.py`, permitiendo múltiples intentos en caso de errores de acceso a bloques.
- Se normalizaron los nombres de bloques para evitar omisiones en la exportación de referencias cruzadas.
- Se actualizaron los logs de ejecución en `log_x0_main.txt` para reflejar el aumento en el número de archivos XML procesados.
2025-06-13 13:01:29 +02:00
29 changed files with 46789 additions and 8109 deletions

293
.doc/backend_setup.md Normal file
View File

@ -0,0 +1,293 @@
# Guía de Configuración para Scripts Backend
## Introducción
Esta guía explica cómo configurar y usar correctamente la función `load_configuration()` en scripts ubicados bajo el directorio `/backend`. La función carga configuraciones desde un archivo `script_config.json` ubicado en el mismo directorio que el script que la llama.
## Configuración del Path e Importación
### 1. Configuración estándar del Path
Para scripts ubicados en subdirectorios bajo `/backend`, usa este patrón estándar:
```python
import os
import sys
# Configurar el path al directorio raíz del proyecto
script_root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
)
sys.path.append(script_root)
# Importar la función de configuración
from backend.script_utils import load_configuration
```
**Nota:** El número de `os.path.dirname()` anidados depende de la profundidad del script:
- Scripts en `/backend/script_groups/grupo/`: 4 niveles
- Scripts en `/backend/`: 2 niveles
### 2. Importación Correcta
**✅ Correcto:**
```python
from backend.script_utils import load_configuration
```
**❌ Incorrecto:**
```python
from script_utils import load_configuration # No funciona desde subdirectorios
```
## Uso de la Función load_configuration()
### Implementación Básica
```python
def main():
# Cargar configuraciones
configs = load_configuration()
# Obtener el directorio de trabajo
working_directory = configs.get("working_directory", "")
# Acceder a configuraciones por nivel
level1_config = configs.get("level1", {})
level2_config = configs.get("level2", {})
level3_config = configs.get("level3", {})
# Ejemplo de uso de parámetros específicos con valores por defecto
scl_output_dir = level2_config.get("scl_output_dir", "scl_output")
xref_output_dir = level2_config.get("xref_output_dir", "xref_output")
if __name__ == "__main__":
main()
```
### Estructura del Archivo script_config.json
El archivo `script_config.json` debe estar ubicado en el mismo directorio que el script que llama a `load_configuration()`. Estructura recomendada:
```json
{
"working_directory": "/ruta/al/directorio/de/trabajo",
"level1": {
"parametro_global_1": "valor1",
"parametro_global_2": "valor2"
},
"level2": {
"scl_output_dir": "scl_output",
"xref_output_dir": "xref_output",
"xref_source_subdir": "source",
"aggregated_filename": "full_project_representation.md"
},
"level3": {
"parametro_especifico_1": true,
"parametro_especifico_2": 100
}
}
```
## Ejemplo Completo de Implementación
```python
"""
Script de ejemplo que demuestra el uso completo de load_configuration()
"""
import os
import sys
import json
# Configuración del path
script_root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
)
sys.path.append(script_root)
from backend.script_utils import load_configuration
def main():
print("=== Cargando Configuración ===")
# Cargar configuraciones
configs = load_configuration()
# Verificar que se cargó correctamente
if not configs:
print("Error: No se pudo cargar la configuración")
return
# Obtener configuraciones
working_directory = configs.get("working_directory", "")
level1_config = configs.get("level1", {})
level2_config = configs.get("level2", {})
level3_config = configs.get("level3", {})
# Mostrar configuraciones cargadas
print(f"Directorio de trabajo: {working_directory}")
print("Configuración Nivel 1:", json.dumps(level1_config, indent=2))
print("Configuración Nivel 2:", json.dumps(level2_config, indent=2))
print("Configuración Nivel 3:", json.dumps(level3_config, indent=2))
# Ejemplo de uso de parámetros con valores por defecto
scl_output_dir = level2_config.get("scl_output_dir", "scl_output")
xref_output_dir = level2_config.get("xref_output_dir", "xref_output")
print(f"Directorio de salida SCL: {scl_output_dir}")
print(f"Directorio de salida XREF: {xref_output_dir}")
if __name__ == "__main__":
main()
```
## Manejo de Errores
La función `load_configuration()` maneja automáticamente los siguientes casos:
1. **Archivo no encontrado**: Retorna un diccionario vacío `{}`
2. **JSON inválido**: Retorna un diccionario vacío y muestra un mensaje de error
3. **Errores de lectura**: Retorna un diccionario vacío y muestra un mensaje de error
### Verificación de Configuración Válida
```python
configs = load_configuration()
# Verificar que se cargó correctamente
if not configs:
print("Advertencia: No se pudo cargar la configuración, usando valores por defecto")
working_directory = "."
else:
working_directory = configs.get("working_directory", ".")
# Verificar directorio de trabajo
if not os.path.exists(working_directory):
print(f"Error: El directorio de trabajo no existe: {working_directory}")
return
```
## Mejores Prácticas
1. **Siempre proporciona valores por defecto** al usar `.get()`:
```python
valor = config.get("clave", "valor_por_defecto")
```
2. **Verifica la existencia de directorios críticos**:
```python
if not os.path.exists(working_directory):
print(f"Error: Directorio no encontrado: {working_directory}")
return
```
3. **Documenta los parámetros esperados** en tu script:
```python
# Parámetros esperados en level2:
# - scl_output_dir: Directorio de salida para archivos SCL
# - xref_output_dir: Directorio de salida para referencias cruzadas
```
4. **Usa nombres de parámetros consistentes** en todos los scripts del mismo grupo.
## Definición Técnica de load_configuration()
```python
def load_configuration() -> Dict[str, Any]:
"""
Load configuration from script_config.json in the current script directory.
Returns:
Dict containing configurations with levels 1, 2, 3 and working_directory
Example usage in scripts:
from backend.script_utils import load_configuration
configs = load_configuration()
level1_config = configs.get("level1", {})
level2_config = configs.get("level2", {})
level3_config = configs.get("level3", {})
working_dir = configs.get("working_directory", "")
"""
```
La función utiliza `inspect.stack()` para determinar automáticamente el directorio del script que la llama, asegurando que siempre busque el archivo `script_config.json` en la ubicación correcta.
## Documentación de Scripts para el Launcher
El sistema de launcher utiliza archivos JSON para mostrar información sobre los grupos de scripts y scripts individuales en la interfaz web.
### Archivo description.json (Descripción del Grupo)
Ubicación: En el directorio raíz del grupo de scripts.
```json
{
"name": "Nombre del Grupo",
"description": "Descripción del propósito y funcionalidad del grupo",
"version": "1.0",
"author": "Nombre del Autor"
}
```
### Archivo scripts_description.json (Descripción de Scripts)
Ubicación: En el directorio raíz del grupo de scripts.
```json
{
"nombre_script.py": {
"display_name": "Nombre para mostrar en la UI",
"short_description": "Descripción breve del script",
"long_description": "Descripción detallada con explicación completa de funcionalidad, pasos que ejecuta, y contexto de uso",
"hidden": false
},
"script_interno.py": {
"display_name": "Script Interno",
"short_description": "Script de uso interno",
"long_description": "",
"hidden": true
}
}
```
### Propiedades Importantes
- **hidden**: `true` oculta el script del launcher (útil para scripts auxiliares)
- **display_name**: Nombre amigable que aparece en la interfaz
- **short_description**: Se muestra en la lista de scripts
- **long_description**: Se muestra al expandir detalles del script
### Ejemplo Práctico
Para un grupo "XML Parser to SCL":
**description.json:**
```json
{
"name": "Siemens-Tia : 03 : Procesador de XML LAD-SCL-AWL",
"description": "Scripts que procesan archivos XML exportados de TIA, convirtiendo LAD a SCL",
"version": "1.0",
"author": "Miguel"
}
```
**scripts_description.json:**
```json
{
"x0_main.py": {
"display_name": "1: Procesar Exportación XML completa",
"short_description": "Conversor principal de LAD/FUP XML a SCL",
"long_description": "Script orquestador que procesa todos los archivos XML...",
"hidden": false
},
"x1_to_json.py": {
"display_name": "x1_to_json",
"short_description": "Converter XML interno",
"long_description": "",
"hidden": true
}
}
```

File diff suppressed because it is too large Load Diff

247
app.py
View File

@ -2,6 +2,7 @@ from flask import Flask, render_template, request, jsonify, url_for
from flask_sock import Sock
from lib.config_manager import ConfigurationManager
from lib.launcher_manager import LauncherManager
from lib.csharp_launcher_manager import CSharpLauncherManager
import os
import json # Added import
from datetime import datetime
@ -27,6 +28,9 @@ config_manager = ConfigurationManager()
# Inicializar launcher manager
launcher_manager = LauncherManager(config_manager.data_path)
# Inicializar C# launcher manager
csharp_launcher_manager = CSharpLauncherManager(config_manager.data_path)
# Lista global para mantener las conexiones WebSocket activas
websocket_connections = set()
@ -813,7 +817,179 @@ def handle_unhandled_exception(e):
return "<h1>Internal Server Error</h1><p>An unhandled error occurred.</p>", 500
# === FIN LAUNCHER GUI APIs ===
# === C# LAUNCHER APIs ===
@app.route("/api/csharp-projects", methods=["GET", "POST"])
def handle_csharp_projects():
"""Gestionar proyectos C# (GET: obtener, POST: crear)"""
if request.method == "GET":
try:
projects = csharp_launcher_manager.get_csharp_projects()
return jsonify(projects)
except Exception as e:
return jsonify({"error": str(e)}), 500
else: # POST
try:
data = request.json
result = csharp_launcher_manager.add_csharp_project(data)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/csharp-projects/<project_id>", methods=["GET", "PUT", "DELETE"])
def handle_csharp_project(project_id):
"""Gestionar proyecto C# específico (GET: obtener, PUT: actualizar, DELETE: eliminar)"""
if request.method == "GET":
try:
project = csharp_launcher_manager.get_csharp_project(project_id)
if not project:
return jsonify({"error": "Project not found"}), 404
return jsonify(project)
except Exception as e:
return jsonify({"error": str(e)}), 500
elif request.method == "PUT":
try:
data = request.json
result = csharp_launcher_manager.update_csharp_project(project_id, data)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
else: # DELETE
try:
result = csharp_launcher_manager.delete_csharp_project(project_id)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/csharp-executables/<project_id>")
def get_csharp_executables(project_id):
"""Obtener ejecutables de un proyecto C#"""
try:
executables = csharp_launcher_manager.get_project_executables(project_id)
return jsonify(executables)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/execute-csharp-executable", methods=["POST"])
def execute_csharp_executable():
"""Ejecutar ejecutable C# con argumentos opcionales"""
try:
data = request.json
project_id = data["project_id"]
exe_name = data["exe_name"]
exe_args = data.get("args", [])
working_dir = data.get("working_dir", None)
result = csharp_launcher_manager.execute_csharp_executable(
project_id, exe_name, exe_args, broadcast_message, working_dir
)
return jsonify(result)
except Exception as e:
error_msg = f"Error ejecutando ejecutable C#: {str(e)}"
broadcast_message(error_msg)
return jsonify({"error": error_msg}), 500
@app.route("/api/csharp-favorites", methods=["GET", "POST"])
def handle_csharp_favorites():
"""Gestionar favoritos del launcher C#"""
if request.method == "GET":
try:
favorites = csharp_launcher_manager.get_favorites()
return jsonify({"favorites": favorites})
except Exception as e:
return jsonify({"error": str(e)}), 500
else: # POST
try:
data = request.json
project_id = data["project_id"]
exe_name = data["exe_name"]
result = csharp_launcher_manager.toggle_favorite(project_id, exe_name)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/csharp-categories")
def get_csharp_categories():
"""Obtener categorías disponibles del launcher C#"""
try:
categories = csharp_launcher_manager.get_categories()
return jsonify(categories)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/csharp-running-processes")
def get_csharp_running_processes():
"""Obtener procesos C# en ejecución"""
try:
processes = csharp_launcher_manager.get_running_processes()
return jsonify({"processes": processes})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/csharp-process-terminate/<int:pid>", methods=["POST"])
def terminate_csharp_process(pid):
"""Cerrar un proceso C#"""
try:
result = csharp_launcher_manager.terminate_process(pid)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/csharp-process-focus/<int:pid>", methods=["POST"])
def focus_csharp_process(pid):
"""Activar foco de un proceso C#"""
try:
result = csharp_launcher_manager.focus_process(pid)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/csharp-executable-metadata/<project_id>/<exe_name>", methods=["GET", "PUT"])
def handle_csharp_executable_metadata(project_id, exe_name):
"""Gestionar metadatos de ejecutables C# (GET: obtener, PUT: actualizar)"""
if request.method == "GET":
try:
metadata = csharp_launcher_manager.get_executable_metadata(project_id, exe_name)
return jsonify(metadata)
except Exception as e:
return jsonify({"error": str(e)}), 500
else: # PUT
try:
data = request.json
result = csharp_launcher_manager.update_executable_metadata(project_id, exe_name, data)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/csharp-executable-arguments/<project_id>/<exe_name>", methods=["GET", "PUT"])
def handle_csharp_executable_arguments(project_id, exe_name):
"""Gestionar argumentos predefinidos de ejecutables C#"""
if request.method == "GET":
try:
arguments = csharp_launcher_manager.get_executable_arguments(project_id, exe_name)
return jsonify({"arguments": arguments})
except Exception as e:
return jsonify({"error": str(e)}), 500
else: # PUT
try:
data = request.json
result = csharp_launcher_manager.update_executable_arguments(
project_id, exe_name, data.get("arguments", [])
)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/csharp-all-executables/<project_id>")
def get_all_csharp_executables(project_id):
"""Obtener todos los ejecutables de un proyecto C# (incluyendo ocultos)"""
try:
executables = csharp_launcher_manager.get_all_project_executables(project_id)
return jsonify(executables)
except Exception as e:
return jsonify({"error": str(e)}), 500
# === FIN C# LAUNCHER APIs ===
# --- Helper function to find VS Code ---
def find_vscode_executable():
@ -842,10 +1018,10 @@ 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']:
if editor not in ['vscode', 'cursor', 'vs2022']:
return jsonify({
"status": "error",
"message": f"Editor '{editor}' no soportado. Usar 'vscode' o 'cursor'"
"message": f"Editor '{editor}' no soportado. Usar 'vscode', 'cursor' o 'vs2022'"
}), 400
# Determinar directorio según el sistema
@ -869,10 +1045,23 @@ def open_group_in_editor(editor, group_system, group_id):
"status": "error",
"message": f"Directorio del grupo launcher '{group['name']}' no encontrado"
}), 404
elif group_system == 'csharp':
project = csharp_launcher_manager.get_csharp_project(group_id)
if not project:
return jsonify({
"status": "error",
"message": f"Proyecto C# '{group_id}' no encontrado"
}), 404
script_group_path = project["directory"]
if not os.path.isdir(script_group_path):
return jsonify({
"status": "error",
"message": f"Directorio del proyecto C# '{project['name']}' no encontrado"
}), 404
else:
return jsonify({
"status": "error",
"message": f"Sistema de grupo '{group_system}' no válido. Usar 'config' o 'launcher'"
"message": f"Sistema de grupo '{group_system}' no válido. Usar 'config', 'launcher' o 'csharp'"
}), 400
# Definir rutas de ejecutables
@ -902,6 +1091,26 @@ def open_group_in_editor(editor, group_system, group_id):
"message": f"Cursor no encontrado. Intenté en: {', '.join(possible_cursor_paths)}"
}), 404
editor_name = "Cursor"
elif editor == 'vs2022':
# Rutas comunes para Visual Studio 2022
possible_vs_paths = [
r"C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\devenv.exe",
r"C:\Program Files\Microsoft Visual Studio\2022\Professional\Common7\IDE\devenv.exe",
r"C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\devenv.exe",
r"C:\Program Files (x86)\Microsoft Visual Studio\2022\Community\Common7\IDE\devenv.exe"
]
editor_path = None
for path in possible_vs_paths:
if os.path.isfile(path):
editor_path = path
break
if not editor_path:
return jsonify({
"status": "error",
"message": f"Visual Studio 2022 no encontrado. Intenté en: {', '.join(possible_vs_paths)}"
}), 404
editor_name = "Visual Studio 2022"
# Verificar que el ejecutable existe
if not os.path.isfile(editor_path):
@ -954,10 +1163,23 @@ def open_group_folder(group_system, group_id):
"status": "error",
"message": f"Directorio del grupo launcher '{group['name']}' no encontrado"
}), 404
elif group_system == 'csharp':
project = csharp_launcher_manager.get_csharp_project(group_id)
if not project:
return jsonify({
"status": "error",
"message": f"Proyecto C# '{group_id}' no encontrado"
}), 404
script_group_path = project["directory"]
if not os.path.isdir(script_group_path):
return jsonify({
"status": "error",
"message": f"Directorio del proyecto C# '{project['name']}' no encontrado"
}), 404
else:
return jsonify({
"status": "error",
"message": f"Sistema de grupo '{group_system}' no válido. Usar 'config' o 'launcher'"
"message": f"Sistema de grupo '{group_system}' no válido. Usar 'config', 'launcher' o 'csharp'"
}), 400
# Abrir en el explorador según el sistema operativo
@ -1012,10 +1234,23 @@ def get_group_path(group_system, group_id):
"status": "error",
"message": f"Directorio del grupo launcher '{group['name']}' no encontrado"
}), 404
elif group_system == 'csharp':
project = csharp_launcher_manager.get_csharp_project(group_id)
if not project:
return jsonify({
"status": "error",
"message": f"Proyecto C# '{group_id}' no encontrado"
}), 404
script_group_path = project["directory"]
if not os.path.isdir(script_group_path):
return jsonify({
"status": "error",
"message": f"Directorio del proyecto C# '{project['name']}' no encontrado"
}), 404
else:
return jsonify({
"status": "error",
"message": f"Sistema de grupo '{group_system}' no válido. Usar 'config' o 'launcher'"
"message": f"Sistema de grupo '{group_system}' no válido. Usar 'config', 'launcher' o 'csharp'"
}), 400
return jsonify({

View File

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

View File

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

View File

@ -8,5 +8,5 @@
"ObsideanProjectsBase": "\\04-SIDEL"
},
"level3": {},
"working_directory": "D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia"
"working_directory": "C:\\Trabajo\\SIDEL\\13 - E5.007560 - Modifica O&U - SAE235\\Reporte\\IOTags"
}

View File

@ -1,6 +1,8 @@
{
"path": "D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia",
"path": "C:\\Trabajo\\SIDEL\\13 - E5.007560 - Modifica O&U - SAE235\\Reporte\\IOTags",
"history": [
"C:\\Trabajo\\SIDEL\\13 - E5.007560 - Modifica O&U - SAE235\\Reporte\\IOTags",
"C:\\Users\\migue\\OneDrive\\Miguel\\Obsidean\\Trabajo\\VM\\04-SIDEL\\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\\IO",
"D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia",
"D:\\Proyectos\\Scripts\\ParamManagerScripts\\backend\\script_groups\\IO_adaptation\\example",
"C:\\Trabajo\\SIDEL\\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\\Reporte\\TAGsIO\\v2"

View File

@ -106,50 +106,67 @@ def load_path_config(working_directory=None):
return None
def read_markdown_table(file_path):
"""Leer tabla en formato Markdown y convertirla a DataFrame."""
"""Leer todas las tablas en formato Markdown que contengan las columnas requeridas y combinarlas en un DataFrame."""
with open(file_path, 'r', encoding='utf-8') as file:
content = file.read()
# Dividir el contenido en líneas
lines = content.strip().split('\n')
# Encontrar el inicio de la tabla (primera línea que comienza con '|')
table_start = None
for i, line in enumerate(lines):
if line.strip().startswith('|'):
table_start = i
# Encontrar todas las tablas en el archivo
all_tables = []
i = 0
while i < len(lines):
# Buscar el inicio de una tabla (línea que comienza con '|')
while i < len(lines) and not lines[i].strip().startswith('|'):
i += 1
if i >= len(lines):
break
if table_start is None:
print("No se encontró ninguna tabla en el archivo")
return pd.DataFrame()
# Encontrar todas las líneas de la tabla
# Encontrar todas las líneas de esta tabla
table_start = i
table_lines = []
for i in range(table_start, len(lines)):
while i < len(lines):
line = lines[i].strip()
if line.startswith('|'):
table_lines.append(line)
elif not line: # Línea vacía podría indicar el final de la tabla
if i + 1 < len(lines) and not lines[i + 1].strip().startswith('|'):
i += 1
elif not line: # Línea vacía
i += 1
# Verificar si la siguiente línea también es parte de la tabla
if i < len(lines) and lines[i].strip().startswith('|'):
continue
else:
break
else:
break # Si no comienza con '|' y no está vacía, es el final de la tabla
if len(table_lines) < 3: # Necesitamos al menos encabezado, separador y una fila de datos
print("La tabla no tiene suficientes filas")
return pd.DataFrame()
if len(table_lines) >= 3: # Necesitamos al menos encabezado, separador y una fila de datos
all_tables.append((table_start, table_lines))
print(f"Se encontraron {len(all_tables)} tablas en el archivo")
# Procesar cada tabla y verificar si tiene las columnas requeridas
valid_dataframes = []
for table_idx, (table_start, table_lines) in enumerate(all_tables):
print(f"\nProcesando tabla {table_idx + 1} (línea {table_start + 1})")
# Procesar encabezados
header_line = table_lines[0]
separator_line = table_lines[1]
separator_line = table_lines[1] if len(table_lines) > 1 else ""
# Verificar que la segunda línea sea realmente un separador
is_separator = False
if separator_line:
is_separator = all(cell.strip().startswith(':') or cell.strip().startswith('-')
for cell in separator_line.split('|')[1:-1] if cell.strip())
if not is_separator:
print("Advertencia: La segunda línea no parece ser un separador. Se asume que es parte de los datos.")
if not is_separator and len(table_lines) > 1:
print(f"Advertencia: La segunda línea no parece ser un separador en tabla {table_idx + 1}. Se asume que es parte de los datos.")
separator_idx = None
else:
separator_idx = 1
@ -165,6 +182,23 @@ def read_markdown_table(file_path):
headers = [h.strip() for h in header_cells]
print(f"Encabezados detectados: {headers}")
# Verificar si la tabla tiene las columnas requeridas
has_io_column = False
has_tag_column = False
for header in headers:
header_lower = header.lower()
if header_lower == 'io' or 'address' in header_lower:
has_io_column = True
if ('master' in header_lower and 'tag' in header_lower) or header_lower == 'master' or header_lower == 'tag':
has_tag_column = True
if not (has_io_column and has_tag_column):
print(f"Tabla {table_idx + 1} no tiene las columnas requeridas (IO/address y Master Tag/master/tag). Omitiendo...")
continue
print(f"Tabla {table_idx + 1} tiene las columnas requeridas. Procesando...")
# Procesar filas de datos
data_start_idx = 2 if separator_idx == 1 else 1
data = []
@ -195,9 +229,53 @@ def read_markdown_table(file_path):
data.append(row_values)
# Convertir a DataFrame
if data: # Solo si hay datos
df = pd.DataFrame(data, columns=headers)
valid_dataframes.append(df)
print(f"Tabla {table_idx + 1} procesada exitosamente: {len(df)} filas")
else:
print(f"Tabla {table_idx + 1} no tiene datos. Omitiendo...")
return df
# Combinar todas las tablas válidas
if not valid_dataframes:
print("No se encontraron tablas válidas con las columnas requeridas")
return pd.DataFrame()
print(f"\nCombinando {len(valid_dataframes)} tablas válidas...")
# Si solo hay un DataFrame, devolverlo directamente
if len(valid_dataframes) == 1:
combined_df = valid_dataframes[0]
else:
# Si hay múltiples DataFrames, necesitamos combinarlos
# Primero, estandarizar las columnas para que todas tengan los mismos nombres
standardized_dfs = []
for df in valid_dataframes:
# Crear un DataFrame estandarizado
standardized_df = df.copy()
# Estandarizar nombres de columnas
new_columns = {}
for col in df.columns:
col_lower = col.lower()
if col_lower == 'io' or 'address' in col_lower:
new_columns[col] = 'IO'
elif 'master' in col_lower and 'tag' in col_lower:
new_columns[col] = 'Master Tag'
elif col_lower == 'master' or col_lower == 'tag':
new_columns[col] = 'Master Tag'
standardized_df = standardized_df.rename(columns=new_columns)
standardized_dfs.append(standardized_df)
# Combinar todos los DataFrames
combined_df = pd.concat(standardized_dfs, ignore_index=True)
print(f"Tabla combinada final: {len(combined_df)} filas, {len(combined_df.columns)} columnas")
print(f"Columnas finales: {list(combined_df.columns)}")
return combined_df
def create_log_file(output_dir):
"""Crear un archivo de log con timestamp."""
@ -233,18 +311,157 @@ def log_message(log_path, message):
print(f"Error al escribir en el log: {e}")
print(message)
def transform_io_address(address):
def get_io_address_format(address, data_type):
"""
Transform IO addresses according to the required format:
Determinar el formato correcto de dirección IO según el tipo de datos.
Args:
address: Dirección base (ej: "I0.0", "Q0.0", "PEW0", etc.)
data_type: Tipo de datos (Bool, Word, Real, etc.)
Returns:
str: Dirección formateada según el tipo de datos
"""
if not address or not isinstance(address, str):
return address
address = address.strip()
data_type_lower = data_type.lower() if data_type else ""
# Handle Profibus ranges (extract the first number before the range)
profibus_match = re.match(r'^(EW|AW)\s+(\d+)\.\..*$', address)
if profibus_match:
prefix, number = profibus_match.groups()
if prefix == 'EW':
if data_type_lower in ['dword', 'udint', 'dint', 'real']:
return f"%ED{number}"
elif data_type_lower in ['byte', 'usint', 'sint']:
return f"%EB{number}"
else:
return f"%EW{number}" # Word por defecto
elif prefix == 'AW':
if data_type_lower in ['dword', 'udint', 'dint', 'real']:
return f"%AD{number}"
elif data_type_lower in ['byte', 'usint', 'sint']:
return f"%AB{number}"
else:
return f"%AW{number}" # Word por defecto
# Handle simple AW/EW with space (without range)
simple_profibus_match = re.match(r'^(EW|AW)\s+(\d+)$', address)
if simple_profibus_match:
prefix, number = simple_profibus_match.groups()
if prefix == 'EW':
if data_type_lower in ['dword', 'udint', 'dint', 'real']:
return f"%ED{number}"
elif data_type_lower in ['byte', 'usint', 'sint']:
return f"%EB{number}"
else:
return f"%EW{number}" # Word por defecto
elif prefix == 'AW':
if data_type_lower in ['dword', 'udint', 'dint', 'real']:
return f"%AD{number}"
elif data_type_lower in ['byte', 'usint', 'sint']:
return f"%AB{number}"
else:
return f"%AW{number}" # Word por defecto
# Extraer números de las direcciones para determinar el formato según tipo de datos
# Patterns for boolean addresses (mantener formato de bit)
if re.match(r'^[IEQ](\d+)\.(\d+)$', address):
byte_num = re.match(r'^[IEQ](\d+)\.(\d+)$', address).group(1)
bit_num = re.match(r'^[IEQ](\d+)\.(\d+)$', address).group(2)
if address.startswith(('I', 'E')):
if data_type_lower == 'bool':
return f"%E{byte_num}.{bit_num}"
elif data_type_lower in ['byte', 'usint', 'sint']:
return f"%EB{byte_num}"
elif data_type_lower in ['dword', 'udint', 'dint', 'real']:
return f"%ED{byte_num}"
else:
return f"%EW{byte_num}" # Word por defecto
elif address.startswith(('Q', 'A')):
if data_type_lower == 'bool':
return f"%A{byte_num}.{bit_num}"
elif data_type_lower in ['byte', 'usint', 'sint']:
return f"%AB{byte_num}"
elif data_type_lower in ['dword', 'udint', 'dint', 'real']:
return f"%AD{byte_num}"
else:
return f"%AW{byte_num}" # Word por defecto
# Patterns for word addresses
elif re.match(r'^PEW(\d+)$', address):
number = re.match(r'^PEW(\d+)$', address).group(1)
if data_type_lower in ['dword', 'udint', 'dint', 'real']:
return f"%ED{number}"
elif data_type_lower in ['byte', 'usint', 'sint']:
return f"%EB{number}"
else:
return f"%EW{number}" # Word por defecto
elif re.match(r'^PAW(\d+)$', address):
number = re.match(r'^PAW(\d+)$', address).group(1)
if data_type_lower in ['dword', 'udint', 'dint', 'real']:
return f"%AD{number}"
elif data_type_lower in ['byte', 'usint', 'sint']:
return f"%AB{number}"
else:
return f"%AW{number}" # Word por defecto
# Handle addresses that already have % prefix but wrong data type format
elif re.match(r'^%[AE][BWDL](\d+)$', address):
match = re.match(r'^%([AE])([BWDL])(\d+)$', address)
if match:
prefix, current_type, number = match.groups()
if prefix == 'E': # Input
if data_type_lower in ['dword', 'udint', 'dint', 'real']:
return f"%ED{number}"
elif data_type_lower in ['byte', 'usint', 'sint']:
return f"%EB{number}"
else:
return f"%EW{number}" # Word por defecto
elif prefix == 'A': # Output
if data_type_lower in ['dword', 'udint', 'dint', 'real']:
return f"%AD{number}"
elif data_type_lower in ['byte', 'usint', 'sint']:
return f"%AB{number}"
else:
return f"%AW{number}" # Word por defecto
# Si ya está en formato correcto o formato desconocido, devolver tal como está
return address
def transform_io_address(address, data_type=None):
"""
Transform IO addresses according to the required format, considering data type:
For Bool:
- Ixx.x %Exx.x
- Exx.x %Exx.x
- Qxx.x %Axx.x
- Axx.x %Axx.x
For Byte:
- Ixx.x %EBxx
- Qxx.x %ABxx
For Word:
- Ixx.x %EWxx
- Qxx.x %AWxx
- PEWxx %EWxx
- PAWxx %AWxx
- EW xx..xx %EWxx (ranges for Profibus)
- AW xx..xx %AWxx (ranges for Profibus)
For DWord/Real:
- Ixx.x %EDxx
- Qxx.x %ADxx
- PEWxx %EDxx
- PAWxx %ADxx
"""
if data_type:
return get_io_address_format(address, data_type)
# Fallback a la lógica anterior si no se proporciona data_type
if not address or not isinstance(address, str):
return address
@ -307,6 +524,62 @@ def is_bit_type(data_type):
"""Determinar si el tipo de dato es un bit (Bool)."""
return data_type.lower() == 'bool'
def get_memory_address_format(data_type, mem_byte, mem_bit=None):
"""
Determinar el formato de dirección de memoria y el incremento de bytes según el tipo de datos.
Returns:
tuple: (address_format, byte_increment, new_bit)
"""
data_type_lower = data_type.lower()
if data_type_lower == 'bool':
# Para Bool usar formato de bit
if mem_bit is None:
mem_bit = 0
address = f"%M{mem_byte}.{mem_bit}"
new_bit = mem_bit + 1
if new_bit > 7:
new_bit = 0
byte_increment = 1
else:
byte_increment = 0
return address, byte_increment, new_bit
elif data_type_lower in ['byte', 'usint', 'sint']:
# Para Byte usar %MB (1 byte)
address = f"%MB{mem_byte}"
return address, 1, None
elif data_type_lower in ['word', 'uint', 'int']:
# Para Word usar %MW (2 bytes)
address = f"%MW{mem_byte}"
return address, 2, None
elif data_type_lower in ['dword', 'udint', 'dint']:
# Para DWord usar %MD (4 bytes)
address = f"%MD{mem_byte}"
return address, 4, None
elif data_type_lower in ['real', 'lreal']:
# Para Real usar %MD (4 bytes), para LReal usar %ML (8 bytes)
if data_type_lower == 'lreal':
address = f"%ML{mem_byte}"
return address, 8, None
else:
address = f"%MD{mem_byte}"
return address, 4, None
elif data_type_lower in ['time', 'time_of_day', 's5time']:
# Para tipos de tiempo usar %MD (4 bytes)
address = f"%MD{mem_byte}"
return address, 4, None
else:
# Para tipos desconocidos, usar %MW por defecto (2 bytes)
address = f"%MW{mem_byte}"
return address, 2, None
def update_plc_tags(excel_path, md_path, output_path, log_path):
"""
Actualiza el archivo Excel con la información del archivo Markdown.
@ -354,24 +627,25 @@ def update_plc_tags(excel_path, md_path, output_path, log_path):
col_lower = col.lower()
if col_lower == 'io' or 'address' in col_lower:
io_col = col
elif 'master' in col_lower and 'tag' in col_lower:
elif ('master' in col_lower and 'tag' in col_lower) or col_lower == 'master' or col_lower == 'tag':
master_tag_col = col
if not io_col or not master_tag_col:
log_message(log_path, "ERROR: No se pudieron identificar las columnas necesarias en el archivo Markdown")
log_message(log_path, f"Columnas disponibles: {list(md_df.columns)}")
return False
log_message(log_path, f"Columna IO: {io_col}")
log_message(log_path, f"Columna Master Tag: {master_tag_col}")
# Crear un diccionario de mapeo IO desde el Markdown
# Crear un diccionario de mapeo IO desde el Markdown (sin transformar aún)
io_mapping = {}
for _, row in md_df.iterrows():
master_tag = str(row[master_tag_col]).strip()
io_value = str(row[io_col]).strip()
if master_tag and io_value and master_tag != 'nan' and io_value != 'nan':
io_mapping[master_tag] = transform_io_address(io_value)
io_mapping[master_tag] = io_value # Guardar la dirección sin transformar
log_message(log_path, f"Tags mapeados en el archivo Markdown: {len(io_mapping)}")
@ -388,9 +662,20 @@ def update_plc_tags(excel_path, md_path, output_path, log_path):
# Inicializar contadores para direcciones de memoria
input_mem_byte = 3600
input_mem_bit = 0
output_mem_byte = 3800
output_mem_byte = 3900
output_mem_bit = 0
# Validar configuración inicial de memoria
if output_mem_byte <= input_mem_byte:
error_msg = (f"ERROR: Configuración de memoria inválida. "
f"output_mem_byte ({output_mem_byte}) debe ser mayor que input_mem_byte ({input_mem_byte}). "
f"Ajuste los valores iniciales de memoria.")
log_message(log_path, error_msg)
return False
log_message(log_path, f"Configuración de memoria: input_mem_byte={input_mem_byte}, output_mem_byte={output_mem_byte}")
log_message(log_path, f"Espacio disponible para inputs: {output_mem_byte - input_mem_byte} bytes")
# Estadísticas
total_tags = 0
updated_tags = 0
@ -453,7 +738,9 @@ def update_plc_tags(excel_path, md_path, output_path, log_path):
# Verificar si el tag está en el mapeo de IO
if tag_name in io_mapping:
old_address = logical_address_cell.value
new_address = io_mapping[tag_name]
io_raw_address = io_mapping[tag_name]
# Transformar la dirección considerando el tipo de datos
new_address = transform_io_address(io_raw_address, data_type)
logical_address_cell.value = new_address
# Determinar el nuevo path basado en la dirección asignada
@ -479,7 +766,7 @@ def update_plc_tags(excel_path, md_path, output_path, log_path):
relocated_tags[new_path] = relocated_tags.get(new_path, 0) + 1
updated_tags += 1
log_message(log_path, f"Actualizado: {tag_name} | Viejo valor: {old_address} | Nuevo valor: {new_address} | Path: {path_cell.value}")
log_message(log_path, f"Actualizado: {tag_name} | Tipo: {data_type} | Viejo valor: {old_address} | Nuevo valor: {new_address} | Path: {path_cell.value}")
# Si no está en el mapeo, asignar dirección de memoria según configuración
else:
@ -498,34 +785,40 @@ def update_plc_tags(excel_path, md_path, output_path, log_path):
# Asignar dirección de memoria según el tipo (Input/Output)
if path_type == "Input" or (is_input and not is_output):
# Asignar dirección de memoria para entradas
# Verificar que no se exceda el límite de memoria de entrada
new_address, byte_increment, new_bit = get_memory_address_format(
data_type, input_mem_byte, input_mem_bit
)
# Verificar límites antes de asignar
new_input_mem_byte = input_mem_byte + byte_increment
if new_input_mem_byte >= output_mem_byte:
error_msg = (f"ERROR: input_mem_byte ({new_input_mem_byte}) excedería output_mem_byte ({output_mem_byte}). "
f"La memoria de entrada está demasiado cerca de la memoria de salida. "
f"Tag problemático: {tag_name}. "
f"Considere aumentar output_mem_byte o reducir el número de tags de entrada.")
log_message(log_path, error_msg)
return False
if is_bit_type(data_type):
new_address = f"%M{input_mem_byte}.{input_mem_bit}"
input_mem_bit += 1
if input_mem_bit > 7:
input_mem_bit = 0
input_mem_byte += 1
else:
new_address = f"%MW{input_mem_byte}"
input_mem_byte += 2
input_mem_bit = new_bit
input_mem_byte = new_input_mem_byte
else: # Tipo Output o no determinado
# Asignar dirección de memoria para salidas
new_address, byte_increment, new_bit = get_memory_address_format(
data_type, output_mem_byte, output_mem_bit
)
if is_bit_type(data_type):
new_address = f"%M{output_mem_byte}.{output_mem_bit}"
output_mem_bit += 1
if output_mem_bit > 7:
output_mem_bit = 0
output_mem_byte += 1
else:
new_address = f"%MW{output_mem_byte}"
output_mem_byte += 2
output_mem_bit = new_bit
output_mem_byte += byte_increment
relocated_tags[no_used_path] = relocated_tags.get(no_used_path, 0) + 1
old_address = logical_address_cell.value
logical_address_cell.value = new_address
assigned_memory_addresses += 1
log_message(log_path, f"Asignación memoria: {tag_name} | Viejo valor: {old_address} | Nuevo valor: {new_address} | Path: {path_cell.value}")
log_message(log_path, f"Asignación memoria: {tag_name} | Tipo: {data_type} | Viejo valor: {old_address} | Nuevo valor: {new_address} | Path: {path_cell.value}")
# Guardar el archivo actualizado
try:
@ -544,6 +837,17 @@ def update_plc_tags(excel_path, md_path, output_path, log_path):
log_message(log_path, f"Tags relocalizados a {path}: {count}")
log_message(log_path, f"Tags con direcciones de memoria asignadas: {assigned_memory_addresses}")
# Mostrar uso final de memoria
log_message(log_path, "\n" + "=" * 25 + " USO DE MEMORIA " + "=" * 25)
log_message(log_path, f"Memoria de entrada final: {input_mem_byte} (inicio: 3600)")
log_message(log_path, f"Memoria de salida final: {output_mem_byte} (inicio: 3900)")
log_message(log_path, f"Bytes usados para entradas: {input_mem_byte - 3600}")
log_message(log_path, f"Bytes usados para salidas: {output_mem_byte - 3900}")
log_message(log_path, f"Espacio restante entre memorias: {3900 - input_mem_byte} bytes")
if input_mem_byte > 3600 or output_mem_byte > 3900:
log_message(log_path, f"✅ Proceso completado exitosamente sin conflictos de memoria")
return True
def main():

File diff suppressed because it is too large Load Diff

View File

@ -1,199 +1,65 @@
--- Log de Ejecución: x4.py ---
Grupo: ObtainIOFromProjectTia
Directorio de Trabajo: D:\Trabajo\VM\22 - 93841 - Sidel - Tilting\Reporte\TiaExports
Inicio: 2025-06-13 11:14:30
Fin: 2025-06-13 11:16:43
Duración: 0:02:13.165274
Directorio de Trabajo: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\Source
Inicio: 2025-06-19 19:05:36
Fin: 2025-06-19 19:06:33
Duración: 0:00:57.281042
Estado: SUCCESS (Código de Salida: 0)
--- SALIDA ESTÁNDAR (STDOUT) ---
--- Exportador de Referencias Cruzadas de TIA Portal ---
Versión de TIA Portal detectada: 19.0 (de la extensión .ap19)
Proyecto seleccionado: D:/Trabajo/VM/22 - 93841 - Sidel - Tilting/InLavoro/PLC/93841_PLC_28/93841_PLC_28.ap19
Usando directorio base de exportación: D:\Trabajo\VM\22 - 93841 - Sidel - Tilting\Reporte\TiaExports
Proyecto seleccionado: D:/Trabajo/VM/44 - 98050 - Fiera/InLavoro/PLC/98050_PLC_11/98050_PLC_11.ap19
Usando directorio base de exportación: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\Source
Conectando a TIA Portal V19.0...
2025-06-13 11:14:34,713 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Global OpenPortal - Start TIA Portal, please acknowledge the security dialog.
2025-06-13 11:14:34,731 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Global OpenPortal - With user interface
2025-06-19 19:05:42,182 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Global OpenPortal - Start TIA Portal, please acknowledge the security dialog.
2025-06-19 19:05:42,202 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Global OpenPortal - With user interface
Conectado a TIA Portal.
2025-06-13 11:14:58,165 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Portal GetProcessId - Process id: 30140
ID del proceso del Portal: 30140
Abriendo proyecto: 93841_PLC_28.ap19...
2025-06-13 11:14:58,500 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Portal OpenProject - Open project... D:\Trabajo\VM\22 - 93841 - Sidel - Tilting\InLavoro\PLC\93841_PLC_28\93841_PLC_28.ap19
Proyecto abierto exitosamente.
2025-06-13 11:15:29,701 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Project GetPlcs - Found plc VM 1512 with parent name ET 200SP station_1
2025-06-13 11:15:30,551 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Project GetPlcs - Found plc SIDEL Transport Example with parent name S71500/ET200MP station_1
Se encontraron 2 PLC(s). Iniciando proceso de exportación de referencias cruzadas...
2025-06-19 19:05:52,371 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Portal GetProcessId - Process id: 24972
ID del proceso del Portal: 24972
2025-06-19 19:05:52,710 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Portal OpenProject - Open project... D:/Trabajo/VM/44 - 98050 - Fiera/InLavoro/PLC/98050_PLC_11/98050_PLC_11.ap19
--- Procesando PLC: VM 1512 ---
Ocurrió un error inesperado: OpennessAccessException: Error when calling method 'OpenWithUpgrade' of type 'Siemens.Engineering.ProjectComposition'.
[PLC: VM 1512] Exportando referencias cruzadas de bloques de programa...
Destino: D:\Trabajo\VM\22 - 93841 - Sidel - Tilting\Reporte\TiaExports\VM 1512\ProgramBlocks_CR
Se encontraron 201 bloques de programa.
Procesando bloque: FC General COM...
Exportando referencias cruzadas para FC General COM...
Procesando bloque: From_SIDEL...
Exportando referencias cruzadas para From_SIDEL...
Procesando bloque: To_SIDEL...
Exportando referencias cruzadas para To_SIDEL...
Procesando bloque: DB Early Restart Blower...
Exportando referencias cruzadas para DB Early Restart Blower...
Procesando bloque: DB Early Restart Filler...
Exportando referencias cruzadas para DB Early Restart Filler...
Procesando bloque: DB Early Restart SynchroBlock...
Exportando referencias cruzadas para DB Early Restart SynchroBlock...
Procesando bloque: FB Early Restart...
Exportando referencias cruzadas para FB Early Restart...
Procesando bloque: DB Signal Transport...
Exportando referencias cruzadas para DB Signal Transport...
Procesando bloque: FC Signal Transport...
Exportando referencias cruzadas para FC Signal Transport...
Procesando bloque: DB Lube - Dry Ecolab...
Exportando referencias cruzadas para DB Lube - Dry Ecolab...
Procesando bloque: FB Lube - Water/Dry...
Exportando referencias cruzadas para FB Lube - Water/Dry...
Procesando bloque: FB Lube - Dry Ecolab...
Exportando referencias cruzadas para FB Lube - Dry Ecolab...
Procesando bloque: FB Lube - EcoLab VM...
Exportando referencias cruzadas para FB Lube - EcoLab VM...
Procesando bloque: FB Lube - Ecolab...
Exportando referencias cruzadas para FB Lube - Ecolab...
Procesando bloque: DB LUBE - Ecolab...
Exportando referencias cruzadas para DB LUBE - Ecolab...
Procesando bloque: FC Ttop Configuration...
Exportando referencias cruzadas para FC Ttop Configuration...
Procesando bloque: FC Ttop Run...
Exportando referencias cruzadas para FC Ttop Run...
Procesando bloque: FC Ttop Alarms...
Exportando referencias cruzadas para FC Ttop Alarms...
Procesando bloque: DB Ttop Run...
Exportando referencias cruzadas para DB Ttop Run...
Procesando bloque: DB Ttop Motor CFG...
Exportando referencias cruzadas para DB Ttop Motor CFG...
Procesando bloque: DB Ttop Alarm...
Exportando referencias cruzadas para DB Ttop Alarm...
Procesando bloque: FC Ttop Motor 31...
Exportando referencias cruzadas para FC Ttop Motor 31...
Procesando bloque: FC Ttop Motor 32...
Exportando referencias cruzadas para FC Ttop Motor 32...
Procesando bloque: FC Ttop Motor 34...
Exportando referencias cruzadas para FC Ttop Motor 34...
Procesando bloque: FC Ttop Motor 35...
Exportando referencias cruzadas para FC Ttop Motor 35...
Procesando bloque: FC Ttop Motor 36...
Exportando referencias cruzadas para FC Ttop Motor 36...
Procesando bloque: DB Ttop Motor 31...
Exportando referencias cruzadas para DB Ttop Motor 31...
Procesando bloque: DB Ttop Motor 32...
Exportando referencias cruzadas para DB Ttop Motor 32...
Procesando bloque: DB Ttop Motor 34...
Exportando referencias cruzadas para DB Ttop Motor 34...
Procesando bloque: DB Ttop Motor 35...
Exportando referencias cruzadas para DB Ttop Motor 35...
Procesando bloque: DB Ttop Minimotor Cfg 32...
Exportando referencias cruzadas para DB Ttop Minimotor Cfg 32...
Procesando bloque: DB Ttop Minimotor Data 32...
Exportando referencias cruzadas para DB Ttop Minimotor Data 32...
Procesando bloque: DB Ttop Motor 36...
Exportando referencias cruzadas para DB Ttop Motor 36...
Procesando bloque: FB Ttop Dryer...
Exportando referencias cruzadas para FB Ttop Dryer...
Procesando bloque: FB Ttop Energy Saving...
Exportando referencias cruzadas para FB Ttop Energy Saving...
Procesando bloque: FB SKID...
Exportando referencias cruzadas para FB SKID...
Procesando bloque: FC Analog Sensor Process...
Exportando referencias cruzadas para FC Analog Sensor Process...
Procesando bloque: FC Valve...
Exportando referencias cruzadas para FC Valve...
Procesando bloque: FB SpeedRegulation...
Exportando referencias cruzadas para FB SpeedRegulation...
Procesando bloque: FC Simple PID...
Exportando referencias cruzadas para FC Simple PID...
Procesando bloque: FC Scale Real...
Exportando referencias cruzadas para FC Scale Real...
Procesando bloque: FB Correct Speed F/Pulses...
Exportando referencias cruzadas para FB Correct Speed F/Pulses...
ERROR GENERAL al exportar referencias cruzadas para el bloque FB Correct Speed F/Pulses: OpennessAccessException: Unexpected exception - no exception message available.
ERROR al acceder a los bloques de programa para exportar referencias cruzadas: OpennessAccessException: Access to a disposed object of type 'Siemens.Engineering.SW.Blocks.FB' is not possible.
TIA Portal has either been disposed or stopped running.
[PLC: VM 1512] Exportando referencias cruzadas de tablas de variables...
Destino: D:\Trabajo\VM\22 - 93841 - Sidel - Tilting\Reporte\TiaExports\VM 1512\PlcTags_CR
ERROR al acceder a las tablas de variables para exportar referencias cruzadas: SerializationException: No se puede encontrar el ensamblado 'Siemens.Engineering, Version=19.0.0.0, Culture=neutral, PublicKeyToken=d29ec89bac048f84'.
Unable to open the project under path 'D:\Trabajo\VM\44 - 98050 - Fiera\InLavoro\PLC\98050_PLC_11\98050_PLC_11.ap19'.
[PLC: VM 1512] Exportando referencias cruzadas de tipos de datos PLC (UDTs)...
Destino: D:\Trabajo\VM\22 - 93841 - Sidel - Tilting\Reporte\TiaExports\VM 1512\PlcDataTypes_CR
ERROR al acceder a los UDTs para exportar referencias cruzadas: SerializationException: No se puede encontrar el ensamblado 'Siemens.Engineering, Version=19.0.0.0, Culture=neutral, PublicKeyToken=d29ec89bac048f84'.
[PLC: VM 1512] Intentando exportar referencias cruzadas de bloques de sistema...
Destino: D:\Trabajo\VM\22 - 93841 - Sidel - Tilting\Reporte\TiaExports\VM 1512\SystemBlocks_CR
ERROR al acceder/procesar bloques de sistema para exportar referencias cruzadas: SerializationException: No se puede encontrar el ensamblado 'Siemens.Engineering, Version=19.0.0.0, Culture=neutral, PublicKeyToken=d29ec89bac048f84'.
[PLC: VM 1512] Intentando exportar referencias cruzadas de unidades de software...
Destino: D:\Trabajo\VM\22 - 93841 - Sidel - Tilting\Reporte\TiaExports\VM 1512\SoftwareUnits_CR
ERROR al acceder/procesar unidades de software para exportar referencias cruzadas: SerializationException: No se puede encontrar el ensamblado 'Siemens.Engineering, Version=19.0.0.0, Culture=neutral, PublicKeyToken=d29ec89bac048f84'.
An error occurred while opening the project
--- Finalizado el procesamiento del PLC: VM 1512 ---
Ocurrió un error inesperado: OpennessAccessException: Access to a disposed object of type 'Siemens.Engineering.HW.DeviceItemImpl' is not possible.
TIA Portal has either been disposed or stopped running.
Cerrando TIA Portal...
2025-06-13 11:16:43,486 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Portal ClosePortal - Close TIA Portal
TIA Portal cerrado.
The project/library D:\Trabajo\VM\44 - 98050 - Fiera\InLavoro\PLC\98050_PLC_11\98050_PLC_11.ap19 cannot be accessed. It has already been opened by user Miguel on computer CSANUC. Note: If the application was not correctly closed, the open projects and libraries can only be opened again after a 2 minute delay.
Script finalizado.
--- ERRORES (STDERR) ---
2025-06-13 11:16:43,458 [1] ERROR Siemens.TiaPortal.OpennessApi19.Implementations.ProgramBlock ExportCrossReferences -
Siemens.TiaPortal.OpennessContracts.OpennessAccessException: Unexpected exception - no exception message available.
Traceback (most recent call last):
File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 128, in export_plc_cross_references
block.export_cross_references(
ValueError: OpennessAccessException: Unexpected exception - no exception message available.
2025-06-13 11:16:43,462 [1] ERROR Siemens.TiaPortal.OpennessApi19.Implementations.ProgramBlock GetName -
Siemens.TiaPortal.OpennessContracts.OpennessAccessException: Access to a disposed object of type 'Siemens.Engineering.SW.Blocks.FB' is not possible.
2025-06-19 19:05:53,136 [1] ERROR Siemens.TiaPortal.OpennessApi19.Implementations.Portal OpenProject -
Siemens.TiaPortal.OpennessContracts.OpennessAccessException: Error when calling method 'OpenWithUpgrade' of type 'Siemens.Engineering.ProjectComposition'.
TIA Portal has either been disposed or stopped running.
Traceback (most recent call last):
File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 124, in export_plc_cross_references
block_name = block.get_name()
^^^^^^^^^^^^^^^^
ValueError: OpennessAccessException: Access to a disposed object of type 'Siemens.Engineering.SW.Blocks.FB' is not possible.
Unable to open the project under path 'D:\Trabajo\VM\44 - 98050 - Fiera\InLavoro\PLC\98050_PLC_11\98050_PLC_11.ap19'.
TIA Portal has either been disposed or stopped running.
An error occurred while opening the project
The project/library D:\Trabajo\VM\44 - 98050 - Fiera\InLavoro\PLC\98050_PLC_11\98050_PLC_11.ap19 cannot be accessed. It has already been opened by user Miguel on computer CSANUC. Note: If the application was not correctly closed, the open projects and libraries can only be opened again after a 2 minute delay.
Traceback (most recent call last):
File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 164, in export_plc_cross_references
tag_tables = plc.get_plc_tag_tables()
^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: SerializationException: No se puede encontrar el ensamblado 'Siemens.Engineering, Version=19.0.0.0, Culture=neutral, PublicKeyToken=d29ec89bac048f84'.
Traceback (most recent call last):
File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 207, in export_plc_cross_references
udts = plc.get_user_data_types()
^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: SerializationException: No se puede encontrar el ensamblado 'Siemens.Engineering, Version=19.0.0.0, Culture=neutral, PublicKeyToken=d29ec89bac048f84'.
Traceback (most recent call last):
File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 251, in export_plc_cross_references
system_blocks = plc.get_system_blocks()
^^^^^^^^^^^^^^^^^^^^^^^
ValueError: SerializationException: No se puede encontrar el ensamblado 'Siemens.Engineering, Version=19.0.0.0, Culture=neutral, PublicKeyToken=d29ec89bac048f84'.
Traceback (most recent call last):
File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 305, in export_plc_cross_references
software_units = plc.get_software_units()
^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: SerializationException: No se puede encontrar el ensamblado 'Siemens.Engineering, Version=19.0.0.0, Culture=neutral, PublicKeyToken=d29ec89bac048f84'.
Traceback (most recent call last):
File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 415, in <module>
export_plc_cross_references(
File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 105, in export_plc_cross_references
plc_name = plc.get_name()
^^^^^^^^^^^^^^
ValueError: OpennessAccessException: Access to a disposed object of type 'Siemens.Engineering.HW.DeviceItemImpl' is not possible.
File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 455, in <module>
portal_instance, project_object = open_portal_and_project(tia_version, project_file)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 413, in open_portal_and_project
project_obj = portal.open_project(project_file_path=str(project_file_path))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: OpennessAccessException: Error when calling method 'OpenWithUpgrade' of type 'Siemens.Engineering.ProjectComposition'.
TIA Portal has either been disposed or stopped running.
Unable to open the project under path 'D:\Trabajo\VM\44 - 98050 - Fiera\InLavoro\PLC\98050_PLC_11\98050_PLC_11.ap19'.
An error occurred while opening the project
The project/library D:\Trabajo\VM\44 - 98050 - Fiera\InLavoro\PLC\98050_PLC_11\98050_PLC_11.ap19 cannot be accessed. It has already been opened by user Miguel on computer CSANUC. Note: If the application was not correctly closed, the open projects and libraries can only be opened again after a 2 minute delay.
--- FIN DEL LOG ---

View File

@ -5,5 +5,5 @@
},
"level2": {},
"level3": {},
"working_directory": "D:\\Trabajo\\VM\\22 - 93841 - Sidel - Tilting\\Reporte\\TiaExports"
"working_directory": "D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source"
}

View File

@ -1,8 +1,10 @@
{
"path": "D:\\Trabajo\\VM\\22 - 93841 - Sidel - Tilting\\Reporte\\TiaExports",
"path": "D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source",
"history": [
"D:\\Trabajo\\VM\\22 - 93841 - Sidel - Tilting\\Reporte\\TiaExports",
"D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source",
"D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia",
"C:\\Trabajo\\SIDEL\\13 - E5.007560 - Modifica O&U - SAE235\\Reporte\\ExportTia",
"D:\\Trabajo\\VM\\22 - 93841 - Sidel - Tilting\\Reporte\\TiaExports",
"C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\SourceDoc\\SourcdSD",
"C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\SourceDoc\\SourceXML"
]

View File

@ -29,6 +29,28 @@ SUPPORTED_TIA_VERSIONS = {
# Using 1 to export all. 0 might also work as a default in some API versions.
CROSS_REF_FILTER = 1
MAX_REOPEN_ATTEMPTS = 5 # Número máximo de re-aperturas permitidas para evitar bucles infinitos
class PortalDisposedException(Exception):
"""Excepción lanzada cuando TIA Portal se ha cerrado inesperadamente o un objeto ha sido descartado."""
def __init__(self, original_exception, failed_block: str | None = None):
super().__init__(str(original_exception))
self.failed_block = failed_block
def _is_disposed_exception(exc: Exception) -> bool:
"""Devuelve True si la excepción proviene de un objeto/portal ya cerrado o sin mensaje útil."""
msg = str(exc).lower()
return any(
indicator in msg
for indicator in (
"disposed object",
"tia portal has either been disposed",
"unexpected exception - no exception message available",
)
)
# --- TIA Scripting Import Handling ---
if os.getenv("TIA_SCRIPTING"):
sys.path.append(os.getenv("TIA_SCRIPTING"))
@ -100,8 +122,25 @@ def select_project_file():
sys.exit(0)
return file_path
def export_plc_cross_references(plc, export_base_dir):
"""Exports cross-references for various elements from a given PLC."""
# Normalizar nombres de bloque/tabla/udt para comparaciones consistentes
def _normalize_name(name: str) -> str:
"""Normaliza un nombre quitando espacios laterales y convirtiendo a minúsculas."""
return name.strip().lower()
def export_plc_cross_references(plc, export_base_dir, exported_blocks=None, problematic_blocks=None):
"""Exports cross-references for various elements from a given PLC.
Parámetros
----------
plc : objeto PLC actual
export_base_dir : pathlib.Path directorio base para exportar
exported_blocks : set[str] bloques que ya se han exportado
problematic_blocks : set[str] bloques que se deben omitir tras fallos previos
"""
if exported_blocks is None:
exported_blocks = set()
if problematic_blocks is None:
problematic_blocks = set()
plc_name = plc.get_name()
print(f"\n--- Procesando PLC: {plc_name} ---")
@ -122,6 +161,14 @@ def export_plc_cross_references(plc, export_base_dir):
print(f" Se encontraron {len(program_blocks)} bloques de programa.")
for block in program_blocks:
block_name = block.get_name()
norm_block = _normalize_name(block_name)
if norm_block in problematic_blocks:
print(f" Omitiendo bloque problemático previamente detectado: {block_name}")
blocks_cr_skipped += 1
continue
if norm_block in exported_blocks:
# Ya exportado en un intento anterior, no repetir
continue
print(f" Procesando bloque: {block_name}...")
try:
print(f" Exportando referencias cruzadas para {block_name}...")
@ -130,10 +177,12 @@ def export_plc_cross_references(plc, export_base_dir):
filter=CROSS_REF_FILTER,
)
blocks_cr_exported += 1
exported_blocks.add(norm_block)
except RuntimeError as block_ex:
print(
f" ERROR TIA al exportar referencias cruzadas para el bloque {block_name}: {block_ex}"
)
problematic_blocks.add(norm_block)
blocks_cr_skipped += 1
except Exception as block_ex:
print(
@ -141,6 +190,10 @@ def export_plc_cross_references(plc, export_base_dir):
)
traceback.print_exc()
blocks_cr_skipped += 1
if _is_disposed_exception(block_ex):
# Escalamos para que el script pueda re-abrir el Portal y omitir el bloque
problematic_blocks.add(norm_block)
raise PortalDisposedException(block_ex, failed_block=block_name)
print(
f" Resumen de exportación de referencias cruzadas de bloques: Exportados={blocks_cr_exported}, Omitidos/Errores={blocks_cr_skipped}"
)
@ -151,6 +204,8 @@ def export_plc_cross_references(plc, export_base_dir):
except Exception as e:
print(f" ERROR al acceder a los bloques de programa para exportar referencias cruzadas: {e}")
traceback.print_exc()
problematic_blocks.add(_normalize_name(e.__str__()))
raise PortalDisposedException(e)
# --- Export PLC Tag Table Cross-References ---
tags_cr_exported = 0
@ -345,6 +400,23 @@ def export_plc_cross_references(plc, export_base_dir):
print(f"\n--- Finalizado el procesamiento del PLC: {plc_name} ---")
def open_portal_and_project(tia_version: str, project_file_path: str):
"""Abre TIA Portal y el proyecto indicado, devolviendo el portal y el objeto proyecto."""
print(f"\nConectando a TIA Portal V{tia_version}...")
portal = ts.open_portal(
version=tia_version,
portal_mode=ts.Enums.PortalMode.WithGraphicalUserInterface,
)
print("Conectado a TIA Portal.")
print(f"ID del proceso del Portal: {portal.get_process_id()}")
project_obj = portal.open_project(project_file_path=str(project_file_path))
if project_obj is None:
project_obj = portal.get_project()
if project_obj is None:
raise Exception("No se pudo abrir u obtener el proyecto especificado tras la reapertura.")
return portal, project_obj
# --- Main Script ---
if __name__ == "__main__":
@ -380,28 +452,9 @@ if __name__ == "__main__":
try:
# 4. Connect to TIA Portal with detected version
print(f"\nConectando a TIA Portal V{tia_version}...")
portal_instance = ts.open_portal(
version=tia_version,
portal_mode=ts.Enums.PortalMode.WithGraphicalUserInterface,
)
print("Conectado a TIA Portal.")
print(f"ID del proceso del Portal: {portal_instance.get_process_id()}")
portal_instance, project_object = open_portal_and_project(tia_version, project_file)
# 5. Open Project
print(f"Abriendo proyecto: {os.path.basename(project_file)}...")
project_path_obj = Path(project_file)
project_object = portal_instance.open_project(
project_file_path=str(project_path_obj)
)
if project_object is None:
print("El proyecto podría estar ya abierto, intentando obtener el manejador...")
project_object = portal_instance.get_project()
if project_object is None:
raise Exception("No se pudo abrir u obtener el proyecto especificado.")
print("Proyecto abierto exitosamente.")
# 6. Get PLCs
# 5. Get PLCs
plcs = project_object.get_plcs()
if not plcs:
print("No se encontraron dispositivos PLC en el proyecto.")
@ -410,12 +463,63 @@ if __name__ == "__main__":
f"Se encontraron {len(plcs)} PLC(s). Iniciando proceso de exportación de referencias cruzadas..."
)
# 7. Iterate and Export Cross-References for each PLC
# 7. Iterate and Export Cross-References for each PLC con lógica de re-apertura
for plc_device in plcs:
plc_name = plc_device.get_name()
exported_blocks = set()
problematic_blocks = set()
skipped_blocks_report = []
reopen_attempts = 0
while True:
try:
export_plc_cross_references(
plc=plc_device,
export_base_dir=export_base_dir,
exported_blocks=exported_blocks,
problematic_blocks=problematic_blocks,
)
break # Éxito
except PortalDisposedException as pd_ex:
reopen_attempts += 1
failed_block = pd_ex.failed_block
if failed_block:
problematic_blocks.add(_normalize_name(failed_block))
skipped_blocks_report.append(failed_block)
if reopen_attempts > MAX_REOPEN_ATTEMPTS:
print(
f"Se alcanzó el máximo de re-aperturas permitidas ({MAX_REOPEN_ATTEMPTS}) para el PLC '{plc_name}'. Abortando."
)
break
# Intentamos cerrar el portal actual (si existe)
try:
print("Cerrando instancia actual de TIA Portal...")
portal_instance.close_portal()
except Exception:
pass
# Re-abrir portal y proyecto
print(f"Re-abriendo TIA Portal (intento {reopen_attempts}/{MAX_REOPEN_ATTEMPTS})...")
portal_instance, project_object = open_portal_and_project(tia_version, project_file)
# Buscar de nuevo el PLC por nombre
plc_device = None
for _plc in project_object.get_plcs():
if _plc.get_name() == plc_name:
plc_device = _plc
break
if plc_device is None:
print(
f"No se encontró el PLC '{plc_name}' tras la re-apertura. Se aborta su procesamiento."
)
break
# Continuar con el while
continue
if skipped_blocks_report:
print(f"\nBloques problemáticos para el PLC '{plc_name}': {', '.join(set(skipped_blocks_report))}")
print("\nProceso de exportación de referencias cruzadas completado.")

View File

@ -0,0 +1,80 @@
Perfecto. Buscaré documentación, ejemplos y recursos útiles sobre cómo interpretar el archivo .EXP exportado por TwinCAT 2.8, especialmente enfocados en los tokens de lenguaje ladder (LAD) como `_NETWORK`, `_LD_ASSIGN`, `_LD_AND`, `_LD_CONTACT`, etc. Priorizaré repositorios de GitHub y recursos prácticos que ayuden a traducir estos tokens a un pseudocódigo similar a Structured Text (ST), útil para documentación. Te aviso en cuanto tenga los resultados.
# Interpretación de archivos .EXP de TwinCAT 2.8 para lógica Ladder (LD)
## Formato de exportación ASCII en TwinCAT 2.8
TwinCAT 2 (basado en CoDeSys 2.x) permite exportar el código PLC a archivos de texto con extensión `.exp`. Dado que los lenguajes gráficos como Ladder Diagram (LD) o Function Block Diagram (FBD) no tienen una representación textual estándar en IEC 61131-3, TwinCAT utiliza un **formato ASCII propio** para exportar estos POUs. En un archivo `.EXP` exportado, cada red (rung) de un diagrama Ladder se describe mediante una secuencia de *tokens* o palabras clave especiales en texto plano. Estos tokens representan contactos, bobinas, operaciones lógicas y la estructura de las redes. Por ejemplo, el manual oficial indica que las POUs en LD/FBD se pueden guardar como ASCII porque “no existe un formato de texto para esto en IEC 61131-3”, por lo que TwinCAT escribe los objetos seleccionados a un archivo ASCII.
Al exportar en TwinCAT 2.8, puede elegirse exportar cada POU en un archivo separado `<nombre>.exp` (por ejemplo, `Program1.exp`) o combinar todo en un solo archivo. El contenido del `.exp` incluye secciones para variables, listas de símbolos y, lo más importante, el **código Ladder como secuencia de tokens** que representan la lógica. A continuación, detallamos los tokens Ladder más comunes y cómo mapearlos a pseudocódigo legible (similar a Structured Text).
## Tokens del lenguaje Ladder en archivos .EXP
A continuación se listan los principales tokens encontrados en un `.exp` exportado de TwinCAT 2.8 para Ladder, junto con su significado e interpretación:
* **`_NETWORK`** Indica el inicio de una *red* Ladder (un rung). Cada red Ladder comienza con este token. Puede ir seguida de un identificador de red o comentario de rung. Por ejemplo, un `.exp` típico mostrará cada rung separado iniciando con `_NETWORK`. Si existen comentarios de red, suelen aparecer a continuación.
* **`_COMMENT` / `_END_COMMENT`** Delimitan un bloque de comentario. TwinCAT 2 permitía agregar comentarios por red (rung comment) que en el archivo `.exp` aparecen entre `_COMMENT` y `_END_COMMENT`. Este bloque (si existe) contiene el texto del comentario del rung.
* **`_LD_CONTACT`** Representa un **contacto** en Ladder. Va seguido de la referencia de la variable booleana asociada (p. ej. una entrada, bit interno, etc.). Indica un contacto normalmente abierto por defecto, a menos que se especifique lo contrario con un flag de inversión. Inmediatamente después del `_LD_CONTACT <Variable>` suele aparecer un token `_EXPRESSION` que define si el contacto está invertido o no.
* **`_EXPRESSION _POSITIV`** Este par de tokens suele seguir a un contacto o a una asignación para indicar **polaridad positiva** (no invertido). En el caso de un contacto, `_POSITIV` significa que es un contacto normalmente abierto (pasa la energía cuando la variable es TRUE). Si el contacto fuese normalmente cerrado, aparecería un indicador distinto (por ejemplo, `_NEG` u otro flag en lugar de `_POSITIV` en la documentación de terceros se describe este campo como *“si está negado”*, siendo `_POSITIV` el valor cuando **no** está negado). En resumen, `_EXPRESSION _POSITIV` después de `_LD_CONTACT Var` confirma que el contacto `Var` se evalúa directamente (TRUE cuando `Var`=TRUE). Si fuera un contacto negado, veríamos un flag indicando inversión (p.ej., `_EXPRESSION _NEG`), lo que implicaría que en pseudocódigo se interpreta como `NOT Var`.
* **`_LD_AND`** Operador lógico **AND** en Ladder. Este token señala que se realiza una conjunción lógica de las condiciones previas en la red. Por ejemplo, si dos contactos en serie alimentan una bobina, en el `.exp` aparecerá un `_LD_AND` para combinar ambos contactos. Generalmente viene acompañado de `_LD_OPERATOR : N`, donde *N* es el número de operandos que está combinando. Un `_LD_AND` con `_LD_OPERATOR : 2` indica que dos condiciones previas se están combinando con AND (es decir, ambas deben ser TRUE). En pseudocódigo, esto equivale a la operación lógica `Cond1 AND Cond2`. De modo similar existe `_LD_OR` (no mostrado arriba pero presente en exportaciones con ramas paralelas) para la operación OR lógico entre ramas.
* **`_LD_OR`** (Aunque no aparece en nuestros ejemplos concretos, es análogo a `_LD_AND`.) Representaría una operación OR entre condiciones/paralelos en la red Ladder. Por ejemplo, contactos en paralelo se exportarían con `_LD_OR` y un `_LD_OPERATOR : N` indicando cuántos caminos paralelos se están OR-combinando.
* **`_LD_ASSIGN`** Marca el **fin de la evaluación lógica de la red** y el inicio de las asignaciones a salidas. Es decir, una vez que se han procesado todos los contactos y operaciones lógicas de la red, `_LD_ASSIGN` indica que ese resultado booleano (TRUE/FALSE) se va a asignar a una o varias bobinas de salida. En la exportación, después de `_LD_ASSIGN` típicamente vendrá otra línea `_EXPRESSION _POSITIV` (o `_NEG`) para indicar si el resultado de la red se utiliza tal cual o invertido para las salidas. Por lo general, será `_POSITIV` salvo que se invierta toda la lógica del rung (situación poco común).
* **`_OUTPUTS : N`** Indica el **número de salidas (bobinas) en esta red**. Si un rung Ladder tiene varias bobinas en paralelo que dependen de la misma lógica de contactos (por ejemplo, bobinas paralelas), aquí se listarán cuántas son. En la mayoría de redes Ladder típicas N=1 (una bobina al final del rung). Un ejemplo del formato exportado: `_OUTPUTS : 1 --1 个输出` significa “1 salida”. Si hubiera, por ejemplo, dos bobinas en paralelo, veríamos `_OUTPUTS : 2`.
* **`_OUTPUT`** Define una **bobina de salida** (coil) a activar. Tras `_OUTPUT` se indican flags que describen el tipo de bobina y su polaridad, seguidos del nombre de la variable de salida asociada. Por ejemplo: `_OUTPUT _POSITIV _NO_SET D0001`. Aquí `_POSITIV` indica que la bobina no está invertida (es una bobina “normalmente desactivada”, energizada directamente con el resultado TRUE de la lógica) y `_NO_SET` indica que es una **bobina regular** (no del tipo Set/Reset). Finalmente `D0001` sería el nombre o dirección de la variable de esa bobina. En un contexto real, en lugar de `D0001` aparecería el nombre de la salida (por ejemplo `MotorOn`) o la dirección (%QX etc., dependiendo de cómo se exporten las variables).
* **Bobinas Set/Reset:** Si la bobina fuera del tipo *latch* (enganche) de Set/Reset, en lugar de `_NO_SET` aparecería otro token. Por ejemplo, es esperable `_SET` para una bobina de *Set* dominante y `_RESET` para una de *Reset*. En la documentación no oficial se observa que `_NO_SET` se usa para bobinas normales, por lo que presumiblemente existen `_SET`/`_NO_RESET` como flags alternativos. Asimismo, la polaridad `_POSITIV` podría cambiar a `_NEG` si se tratara de una bobina negada (una bobina especial energizada cuando la condición es FALSE). En general: `_OUTPUT _POSITIV _NO_SET Var` corresponde a `Var := Resultado_logico` cuando la lógica es TRUE (bobina estándar), mientras que una variante `_OUTPUT _POSITIV _SET Var` significaría que `Var` se *establece (latchea)* a TRUE con la condición, y `_OUTPUT _POSITIV _RESET Var` que `Var` se resetea con la condición.
* **`END_PROGRAM`** Marca el fin del bloque de programa exportado. El archivo `.exp` típico comienza con la declaración del POU (p. ej. `PROGRAM NombreProg LD`) y finaliza con `END_PROGRAM` una vez listadas todas las redes Ladder y salidas. Todo lo descrito entre estos delimitadores corresponde al contenido del POU Ladder en formato textual.
**Ejemplo ilustrativo:** En un foro técnico se mostró un fragmento de `.exp` resultante de exportar Ladder, que ayuda a visualizar varios de estos tokens y su secuencia. Por ejemplo:
```plaintext
_LD_CONTACT A0001 (... variable de entrada ...)
_LD_CONTACT A0002 (... otra entrada ...)
_LD_AND
_LD_OPERATOR : 2 ; AND de 2 operandos (A0001 y A0002)
_LD_ASSIGN
_OUTPUTS : 1 ; Una salida en esta red
_OUTPUT _POSITIV _NO_SET D0001
END_PROGRAM
```
En este caso hipotético, `A0001` y `A0002` podrían ser dos contactos en serie y `D0001` una bobina de salida. Los tokens indican: carga dos contactos (`_LD_CONTACT`), combínalos con un AND de 2 entradas (`_LD_AND` + `_LD_OPERATOR:2`), asigna el resultado (`_LD_ASSIGN`) a 1 salida (`_OUTPUTS:1`), que es una bobina normal no invertida (`_OUTPUT _POSITIV _NO_SET`) asignada a la variable D0001.
Del mismo modo, otro ejemplo simple tomado de la documentación no oficial muestra la estructura para una red con **un contacto y una bobina** únicamente: primero el contacto y su variable, luego la asignación y la bobina de salida. Allí se observa `_LD_CONTACT p1` seguido de `_EXPRESSION _POSITIV` (contacto normalmente abierto con variable **p1**), luego `_LD_ASSIGN` con `_EXPRESSION _POSITIV` y finalmente `_OUTPUTS:1` con `_OUTPUT _POSITIV _NO_SET p2` para energizar la variable **p2**. Esta red equivale a una lógica donde *p2 = p1*, es decir, la bobina p2 se activa cuando la entrada p1 está activa.
## Mapeo de la lógica Ladder a pseudocódigo (Structured Text)
El objetivo de interpretar estos tokens es poder traducir la lógica Ladder en texto entendible, similar a Structured Text (ST) o pseudocódigo, para facilitar la documentación. Básicamente, se trata de reconstruir las expresiones booleanas y asignaciones a partir de la secuencia de tokens:
* **Contactos:** Cada `_LD_CONTACT Var` se convierte en una condición booleana sobre `Var`. Si el token va seguido de `_POSITIV`, significa que la condición es simplemente `Var` (TRUE cuando la variable es TRUE). Si estuviera negado, la condición sería `NOT Var`. En pseudocódigo ST podemos representar un contacto normalmente abierto como `Var` y uno normalmente cerrado como `NOT Var`.
* **Operadores lógicos AND/OR:** Tokens como `_LD_AND` con `_LD_OPERATOR:n` indican combinaciones lógicas. Por ejemplo, si hay dos contactos seguidos de `_LD_AND`, en ST sería una conjunción: `Cond1 AND Cond2`. Si hubiera `_LD_OR`, sería una disyunción: `Cond1 OR Cond2`. Estos operadores reflejan ramas en serie (AND) o en paralelo (OR) en el esquema Ladder. Por ejemplo, `_LD_AND` con 2 operandos se traduce como `ExpresionResultado = (Expr1 AND Expr2)`.
* **Asignación a salidas:** El token `_LD_ASSIGN` señala que la expresión lógica formada por los contactos y operadores anteriores ya determina el resultado del rung. En Ladder, ese resultado se envía a una o varias bobinas. En pseudocódigo, esto corresponde a realizar asignaciones a las variables de salida. Si `_OUTPUTS : 1`, hay una sola salida y simplemente pondremos esa variable igual a la expresión booleana resultante. Si hay múltiples salidas (p. ej. dos bobinas en paralelo), cada una recibirá el mismo valor de la expresión lógica. Por ejemplo, si la lógica calculada es `Expr` y hay dos salidas `Out1` y `Out2`, en ST podríamos escribir: `Out1 := Expr; Out2 := Expr;`.
* **Bobinas (coils):** Un `_OUTPUT _POSITIV _NO_SET Var` se interpreta como una asignación directa: `Var := ResultadoLogico`. Si la bobina estuviera invertida (`_NEG`), equivaldría a `Var := NOT(ResultadoLogico)`. Si es un coil de *Set*, en Ladder significa que cuando la condición es TRUE se *establece* la variable (la mantiene a 1 hasta otro evento), lo cual en pseudocódigo se modelaría con algo como `IF Resultado THEN Var := TRUE; END_IF` (y análogamente un coil de Reset con `IF Resultado THEN Var := FALSE; END_IF`). No obstante, Ladder maneja set/reset de forma interna, por lo que para documentación suele ser suficiente indicar “(Set)” o “(Reset)” junto a la asignación.
* **Rung completo:** En conjunto, cada `_NETWORK` puede traducirse a un bloque *IF/THEN* o a una expresión booleana asignada. Una forma de documentarlo estilo ST es escribir la ecuación booleana de la red. Por ejemplo, considerando el fragmento anterior con dos contactos en serie asignando una bobina `Motor1`, la pseudocódigo podría ser: `Motor1 := A0001 AND A0002;` (suponiendo `A0001` y `A0002` son variables booleanas). Si hubiera contactos en paralelo (OR), se agruparían con paréntesis adecuadamente. Alternativamente, se puede expresarlo como lógica condicional:
```st
IF (A0001 AND A0002) THEN
Motor1 := TRUE;
ELSE
Motor1 := FALSE;
END_IF;
```
Ambas formas representan la misma lógica de la red Ladder en un formato textual claro.
**Notas:** También existen tokens para construcciones especiales. Por ejemplo, `_JUMP <etiqueta>` puede aparecer en `.exp` para reflejar instrucciones de salto (gotos) dentro de Ladder *il* o saltos condicionales (similar a instrucciones en lenguaje IL) aunque en Ladder puro estándar no son comunes, CoDeSys permitía elementos como `jump`. Otro posible token es `_EN`/`_ENO` para conexiones de habilitación a cajas de función (FB/funciones) insertadas en Ladder. Estos casos avanzados van más allá de simples contactos y bobinas, pero siguen una lógica similar: el `.exp` listará llamados a funciones o saltos con sus parámetros en texto. Si el objetivo es documentación, normalmente se enfoca en la lógica combinacional de contactos y bobinas, que es lo descrito arriba.
## Herramientas y recursos para la conversión
Encontrar documentación detallada de este formato no estándar puede ser difícil, pero existen **recursos oficiales y de la comunidad** que ayudan a interpretarlo. Beckhoff no publica abiertamente la gramática completa de `.exp`, pero la información fragmentada en manuales y foros nos da guía. Por ejemplo, un manual de HollySys (un PLC basado en CoDeSys) incluye una sección explicando cada token Ladder (\_LD\_CONTACT, \_LD\_AND, \_OUTPUT, etc.) y cómo corresponden a los elementos gráficos. Aunque esté en chino, confirma la semántica: por ejemplo, `_LD_CONTACT --触点标识... _EXPRESSION --是否置反标识 _POSITIV` significa que `_LD_CONTACT` identifica un contacto y `_POSITIV` indica que **no** está negado. Del mismo modo, muestra `_OUTPUT ... _POSITIV _NO_SET ...` para una bobina normal. Este tipo de documentación no oficial puede servir de referencia de mapeo.
En cuanto a herramientas automáticas para convertir `.exp` Ladder a código legible o ST, **no se conocen utilidades públicas específicas** enfocadas solo en TwinCAT 2 `.exp`. Sin embargo, hay enfoques posibles:
* **Uso de TwinCAT 3/PLCopen:** Beckhoff TwinCAT 3 ofrece un **convertidor de formatos TwinCAT 2** integrado. Es posible importar el proyecto o POU exportado de TwinCAT 2 (archivo `.exp` o `.tpy`) a TwinCAT 3 y luego exportarlo a XML PLCopen, que es un formato estándar. El XML PLCopen describirá la lógica Ladder de forma estructurada, más fácil de leer o de procesar con scripts (por ejemplo, extrayendo las ecuaciones lógicas). De hecho, un experto sugiere usar PLCopen XML como vía de intercambio entre plataformas. Esto requeriría tener TwinCAT 3 instalado para la conversión, pero puede ahorrar tiempo si se dispone de muchos rungs.
* **Scripting personalizado:** Dado que el `.exp` es texto ASCII, se puede escribir un script (en Python u otro lenguaje) para *parsear* los tokens y regenerar la lógica. La gramática es relativamente simple (como se detalló arriba). Por ejemplo, uno podría leer línea a línea, identificar bloques `_NETWORK ... _OUTPUTS` y construir la expresión booleana intermedia. No encontramos una librería Python ya hecha para esto, pero es viable implementarlo. Algunos usuarios en foros han discutido partes del formato precisamente con la idea de traducirlo; por ejemplo, en un foro chino se intentó “traducir un programa Ladder de Siemens (TIA) a Beckhoff” analizando un `.exp` de TwinCAT, lo que evidencia el interés en tales conversiones. Un proyecto open-source relacionado es *Blark* (un parser de código ST de TwinCAT), que aunque está orientado a Structured Text, demuestra que es posible crear gramáticas para lenguajes PLC en Python. Para Ladder, un desarrollador podría definir reglas similares: contactos -> operandos booleanos, `_LD_AND` -> operador AND, etc., o incluso usar expresiones regulares dado el formato estructurado lineal del `.exp`.
* **Recursos comunitarios:** Revisar comunidades de automatización y repositorios GitHub puede dar frutos. Si bien no hallamos una herramienta específica lista para usar, sitios como PLCtalk y Stack Overflow tienen hilos donde se menciona la exportación `.exp`. Por ejemplo, en Stack Overflow se preguntó sobre importar .exp entre distintos entornos CoDeSys, confirmando que `.exp` es un formato Codesys genérico utilizado por múltiples marcas. Esto significa que documentación de **CoDeSys 2.3** también aplica (muchos controladores usaban el mismo formato export). En suma, buscar por “CoDeSys export .exp Ladder” puede arrojar tips de usuarios que hayan hecho ingeniería inversa del formato.
**Conclusión:** Mediante la combinación de fuentes oficiales (manuales de TwinCAT/Codesys) y no oficiales (ejemplos en foros, manuales de terceros), es posible mapear los tokens `_NETWORK`, `_LD_CONTACT`, `_LD_AND`, `_LD_ASSIGN`, `_OUTPUT`, etc., a construcciones lógicas comprensibles. La clave es reconocer la secuencia: una red Ladder en `.exp` corresponde a una expresión booleana (derivada de contactos y operadores) asignada a una o varias salidas. Documentar esa lógica en pseudocódigo estilo ST por ejemplo, escribiendo ecuaciones lógicas o condiciones IF/THEN hará más legible el programa. Con los ejemplos y explicaciones recopilados aquí, se tiene un **guía de referencia** para emprender esa traducción manual o mediante script, facilitando la comprensión de programas Ladder exportados de TwinCAT 2.8.
**Referencias usadas:** Documentación de Beckhoff TwinCAT 2, fragmentos de manual Hollysys/AutoThink, y ejemplos de código `.exp` discutidos en foros, entre otros. Estas fuentes proporcionan casos prácticos y descripciones claras para respaldar la interpretación de cada token y su equivalente lógico. Por último, la utilización de herramientas modernas (TwinCAT 3, PLCopen) o scripts personalizados son caminos recomendados si se requiere automatizar la conversión de `.exp` Ladder a texto estructurado.

View File

@ -0,0 +1,293 @@
# Guía de Configuración para Scripts Backend
## Introducción
Esta guía explica cómo configurar y usar correctamente la función `load_configuration()` en scripts ubicados bajo el directorio `/backend`. La función carga configuraciones desde un archivo `script_config.json` ubicado en el mismo directorio que el script que la llama.
## Configuración del Path e Importación
### 1. Configuración estándar del Path
Para scripts ubicados en subdirectorios bajo `/backend`, usa este patrón estándar:
```python
import os
import sys
# Configurar el path al directorio raíz del proyecto
script_root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
)
sys.path.append(script_root)
# Importar la función de configuración
from backend.script_utils import load_configuration
```
**Nota:** El número de `os.path.dirname()` anidados depende de la profundidad del script:
- Scripts en `/backend/script_groups/grupo/`: 4 niveles
- Scripts en `/backend/`: 2 niveles
### 2. Importación Correcta
**✅ Correcto:**
```python
from backend.script_utils import load_configuration
```
**❌ Incorrecto:**
```python
from script_utils import load_configuration # No funciona desde subdirectorios
```
## Uso de la Función load_configuration()
### Implementación Básica
```python
def main():
# Cargar configuraciones
configs = load_configuration()
# Obtener el directorio de trabajo
working_directory = configs.get("working_directory", "")
# Acceder a configuraciones por nivel
level1_config = configs.get("level1", {})
level2_config = configs.get("level2", {})
level3_config = configs.get("level3", {})
# Ejemplo de uso de parámetros específicos con valores por defecto
scl_output_dir = level2_config.get("scl_output_dir", "scl_output")
xref_output_dir = level2_config.get("xref_output_dir", "xref_output")
if __name__ == "__main__":
main()
```
### Estructura del Archivo script_config.json
El archivo `script_config.json` debe estar ubicado en el mismo directorio que el script que llama a `load_configuration()`. Estructura recomendada:
```json
{
"working_directory": "/ruta/al/directorio/de/trabajo",
"level1": {
"parametro_global_1": "valor1",
"parametro_global_2": "valor2"
},
"level2": {
"scl_output_dir": "scl_output",
"xref_output_dir": "xref_output",
"xref_source_subdir": "source",
"aggregated_filename": "full_project_representation.md"
},
"level3": {
"parametro_especifico_1": true,
"parametro_especifico_2": 100
}
}
```
## Ejemplo Completo de Implementación
```python
"""
Script de ejemplo que demuestra el uso completo de load_configuration()
"""
import os
import sys
import json
# Configuración del path
script_root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
)
sys.path.append(script_root)
from backend.script_utils import load_configuration
def main():
print("=== Cargando Configuración ===")
# Cargar configuraciones
configs = load_configuration()
# Verificar que se cargó correctamente
if not configs:
print("Error: No se pudo cargar la configuración")
return
# Obtener configuraciones
working_directory = configs.get("working_directory", "")
level1_config = configs.get("level1", {})
level2_config = configs.get("level2", {})
level3_config = configs.get("level3", {})
# Mostrar configuraciones cargadas
print(f"Directorio de trabajo: {working_directory}")
print("Configuración Nivel 1:", json.dumps(level1_config, indent=2))
print("Configuración Nivel 2:", json.dumps(level2_config, indent=2))
print("Configuración Nivel 3:", json.dumps(level3_config, indent=2))
# Ejemplo de uso de parámetros con valores por defecto
scl_output_dir = level2_config.get("scl_output_dir", "scl_output")
xref_output_dir = level2_config.get("xref_output_dir", "xref_output")
print(f"Directorio de salida SCL: {scl_output_dir}")
print(f"Directorio de salida XREF: {xref_output_dir}")
if __name__ == "__main__":
main()
```
## Manejo de Errores
La función `load_configuration()` maneja automáticamente los siguientes casos:
1. **Archivo no encontrado**: Retorna un diccionario vacío `{}`
2. **JSON inválido**: Retorna un diccionario vacío y muestra un mensaje de error
3. **Errores de lectura**: Retorna un diccionario vacío y muestra un mensaje de error
### Verificación de Configuración Válida
```python
configs = load_configuration()
# Verificar que se cargó correctamente
if not configs:
print("Advertencia: No se pudo cargar la configuración, usando valores por defecto")
working_directory = "."
else:
working_directory = configs.get("working_directory", ".")
# Verificar directorio de trabajo
if not os.path.exists(working_directory):
print(f"Error: El directorio de trabajo no existe: {working_directory}")
return
```
## Mejores Prácticas
1. **Siempre proporciona valores por defecto** al usar `.get()`:
```python
valor = config.get("clave", "valor_por_defecto")
```
2. **Verifica la existencia de directorios críticos**:
```python
if not os.path.exists(working_directory):
print(f"Error: Directorio no encontrado: {working_directory}")
return
```
3. **Documenta los parámetros esperados** en tu script:
```python
# Parámetros esperados en level2:
# - scl_output_dir: Directorio de salida para archivos SCL
# - xref_output_dir: Directorio de salida para referencias cruzadas
```
4. **Usa nombres de parámetros consistentes** en todos los scripts del mismo grupo.
## Definición Técnica de load_configuration()
```python
def load_configuration() -> Dict[str, Any]:
"""
Load configuration from script_config.json in the current script directory.
Returns:
Dict containing configurations with levels 1, 2, 3 and working_directory
Example usage in scripts:
from backend.script_utils import load_configuration
configs = load_configuration()
level1_config = configs.get("level1", {})
level2_config = configs.get("level2", {})
level3_config = configs.get("level3", {})
working_dir = configs.get("working_directory", "")
"""
```
La función utiliza `inspect.stack()` para determinar automáticamente el directorio del script que la llama, asegurando que siempre busque el archivo `script_config.json` en la ubicación correcta.
## Documentación de Scripts para el Launcher
El sistema de launcher utiliza archivos JSON para mostrar información sobre los grupos de scripts y scripts individuales en la interfaz web.
### Archivo description.json (Descripción del Grupo)
Ubicación: En el directorio raíz del grupo de scripts.
```json
{
"name": "Nombre del Grupo",
"description": "Descripción del propósito y funcionalidad del grupo",
"version": "1.0",
"author": "Nombre del Autor"
}
```
### Archivo scripts_description.json (Descripción de Scripts)
Ubicación: En el directorio raíz del grupo de scripts.
```json
{
"nombre_script.py": {
"display_name": "Nombre para mostrar en la UI",
"short_description": "Descripción breve del script",
"long_description": "Descripción detallada con explicación completa de funcionalidad, pasos que ejecuta, y contexto de uso",
"hidden": false
},
"script_interno.py": {
"display_name": "Script Interno",
"short_description": "Script de uso interno",
"long_description": "",
"hidden": true
}
}
```
### Propiedades Importantes
- **hidden**: `true` oculta el script del launcher (útil para scripts auxiliares)
- **display_name**: Nombre amigable que aparece en la interfaz
- **short_description**: Se muestra en la lista de scripts
- **long_description**: Se muestra al expandir detalles del script
### Ejemplo Práctico
Para un grupo "XML Parser to SCL":
**description.json:**
```json
{
"name": "Siemens-Tia : 03 : Procesador de XML LAD-SCL-AWL",
"description": "Scripts que procesan archivos XML exportados de TIA, convirtiendo LAD a SCL",
"version": "1.0",
"author": "Miguel"
}
```
**scripts_description.json:**
```json
{
"x0_main.py": {
"display_name": "1: Procesar Exportación XML completa",
"short_description": "Conversor principal de LAD/FUP XML a SCL",
"long_description": "Script orquestador que procesa todos los archivos XML...",
"hidden": false
},
"x1_to_json.py": {
"display_name": "x1_to_json",
"short_description": "Converter XML interno",
"long_description": "",
"hidden": true
}
}
```

204
backend/script_groups/TwinCat/.gitignore vendored Normal file
View File

@ -0,0 +1,204 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
*$py.class
# C extensions
*.so
*.scl
*.exp
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
#poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.envrc
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Cursor
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
# refer to https://docs.cursor.com/context/ignore-files
.cursorignore
.cursorindexingignore
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/

View File

@ -0,0 +1,172 @@
# Convertidor LAD a Pseudocódigo Estructurado
## Descripción
Este proyecto proporciona herramientas para convertir código LAD (Ladder Diagram) de TwinCAT a pseudocódigo estructurado compatible con IEC61131-3. El convertidor mantiene la semántica original del código LAD mientras lo transforma a un formato más legible y estructurado.
## Características
- ✅ **Parsing completo de LAD**: Analiza la estructura completa de archivos `.EXP` de TwinCAT
- ✅ **Conversión semántica**: Mantiene la lógica original del diagrama ladder
- ✅ **Formato estructurado**: Genera código pseudo estructurado con IF-THEN-END_IF
- ✅ **Manejo de contactos**: Convierte contactos normales y negados correctamente
- ✅ **Function blocks**: Identifica y convierte llamadas a bloques de función
- ✅ **Operadores matemáticos**: Maneja operaciones aritméticas y lógicas
- ✅ **Comentarios**: Preserva comentarios de las redes originales
## Archivos del Proyecto
### Convertidores Principales
1. **`simple_lad_converter.py`** - Convertidor simplificado y robusto (recomendado)
2. **`lad_to_pseudocode_converter.py`** - Convertidor básico inicial
3. **`lad_to_pseudocode_converter_enhanced.py`** - Versión avanzada con más características
### Archivos de Prueba
- **`test_simple.py`** - Script de prueba para el convertidor simple
- **`ejemplo_conversion.py`** - Ejemplo de uso del convertidor básico
- **`test_enhanced_converter.py`** - Prueba para el convertidor avanzado
## Uso Rápido
### Método Simple (Recomendado)
```bash
python test_simple.py
```
Este comando procesará el archivo `.example/INPUT.EXP` y generará `output_simple.txt` con el código convertido.
### Uso Directo del Convertidor
```python
from simple_lad_converter import SimpleLadConverter
converter = SimpleLadConverter()
converter.parse_file("mi_archivo.EXP")
structured_code = converter.save_to_file("salida.txt")
```
### Línea de Comandos
```bash
python lad_to_pseudocode_converter.py archivo_entrada.EXP archivo_salida.txt
```
## Estructura del Código LAD Soportada
El convertidor puede procesar las siguientes estructuras de TwinCAT:
### Elementos LAD Reconocidos
- `_NETWORK` - Inicio de red
- `_LD_ASSIGN` - Asignaciones
- `_LD_CONTACT` - Contactos (entradas)
- `_LD_AND` / `_LD_OR` - Operaciones lógicas
- `_FUNCTIONBLOCK` - Bloques de función
- `_OPERATOR` - Operadores matemáticos
- `_COMMENT` / `_END_COMMENT` - Comentarios
- `_OUTPUT` - Variables de salida
### Operadores Soportados
- **Aritméticos**: ADD, SUB, MUL, DIV
- **Lógicos**: AND, OR
- **Comparación**: LT, GT, EQ
- **Especiales**: SEL, MOVE
## Ejemplo de Conversión
### Código LAD Original
```
_NETWORK
_COMMENT
Verificación de presión CO2
_END_COMMENT
_LD_ASSIGN
_LD_CONTACT
DI_Air_InletPress_OK
_NEGATIV
_FUNCTIONBLOCK
mAirPressOk
_OUTPUT
gInLinePressAirOk
```
### Código Estructurado Generado
```
// Red 5
// Verificación de presión CO2
IF NOT DI_Air_InletPress_OK THEN
gInLinePressAirOk := mAirPressOk();
END_IF;
```
## Estructura del Archivo de Salida
El código generado sigue esta estructura:
```
// Código pseudo estructurado generado desde LAD TwinCAT
// Compatible con IEC61131-3
PROGRAM Input_Converted
// Red 1
IF condicion1 AND condicion2 THEN
variable_salida := funcion_bloque(parametros);
END_IF;
// Red 2
variable := operando1 ADD operando2;
// Red N...
END_PROGRAM
```
## Resultados de la Conversión
El convertidor ha procesado exitosamente el archivo `INPUT.EXP` que contiene:
- **86 redes LAD** en total
- **Múltiples tipos de elementos**: contactos, function blocks, operadores
- **Preservación de comentarios** originales
- **Conversión correcta de lógica condicional** con IF-THEN-END_IF
### Estadísticas del Ejemplo
- Archivo de entrada: `.example/INPUT.EXP` (4,611 líneas)
- Redes procesadas: 86
- Archivo de salida: ~235 líneas de código estructurado
- Reducción de complejidad: ~95%
## Ventajas del Código Convertido
1. **Legibilidad**: Más fácil de leer que el formato LAD textual
2. **Mantenibilidad**: Estructura clara con comentarios preservados
3. **Debugging**: Lógica condicional explícita
4. **Documentación**: Comentarios de red integrados
5. **Portabilidad**: Formato pseudo-código universal
## Limitaciones Conocidas
- Algunos parámetros internos pueden aparecer como `_POSITIV`, `_NEGATIV`
- Estructuras complejas de LAD pueden requerir revisión manual
- El convertidor es específico para el formato de TwinCAT
## Desarrollo Futuro
- [ ] Mejorar el parsing de parámetros de function blocks
- [ ] Añadir soporte para más tipos de operadores
- [ ] Implementar validación de sintaxis
- [ ] Crear interfaz gráfica para conversión
- [ ] Soporte para otros formatos de PLC
## Contribuciones
Este convertidor fue desarrollado para facilitar el análisis y mantenimiento de código LAD en proyectos de automatización industrial. Las contribuciones y mejoras son bienvenidas.
---
**Nota**: Este es un proyecto de código abierto para ayudar en la migración y análisis de código LAD de TwinCAT a formatos más estructurados.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,17 @@
{
"folders": [
{
"path": "."
},
{
"path": "C:/Program Files/Siemens/Automation/Portal V19/PublicAPI/V19/Schemas"
},
{
"path": "../../../../../../Trabajo/VM/44 - 98050 - Fiera/Reporte/ExportsTia/Source/98050_PLC"
},
{
"path": "../../../../../../Trabajo/VM/22 - 93841 - Sidel - Tilting/Reporte/TiaExports"
}
],
"settings": {}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -15,5 +15,5 @@
"xref_source_subdir": "source"
},
"level3": {},
"working_directory": "D:\\Trabajo\\VM\\22 - 93841 - Sidel - Tilting\\Reporte\\TiaExports"
"working_directory": "D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source"
}

View File

@ -1,8 +1,9 @@
{
"path": "D:\\Trabajo\\VM\\22 - 93841 - Sidel - Tilting\\Reporte\\TiaExports",
"path": "D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source",
"history": [
"D:\\Trabajo\\VM\\22 - 93841 - Sidel - Tilting\\Reporte\\TiaExports",
"D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source",
"C:\\Trabajo\\SIDEL\\13 - E5.007560 - Modifica O&U - SAE235\\Reporte\\ExportTia",
"D:\\Trabajo\\VM\\22 - 93841 - Sidel - Tilting\\Reporte\\TiaExports",
"C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\SourceDoc\\SourceXML",
"C:\\Trabajo\\SIDEL\\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\\Reporte\\IOExport"
]

View File

@ -1,5 +1,31 @@
{
"history": [
{
"id": "b321622a",
"group_id": "4",
"script_name": "x1.py",
"executed_date": "2025-06-15T18:19:14.681042Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/Siemens/Tia Portal Utils",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 27400,
"execution_time": null
},
{
"id": "754d0df9",
"group_id": "4",
"script_name": "x1.py",
"executed_date": "2025-06-15T18:01:45.840069Z",
"arguments": [],
"working_directory": "D:/Proyectos/Scripts/Siemens/Tia Portal Utils",
"python_env": "tia_scripting",
"executable_type": "pythonw.exe",
"status": "running",
"pid": 38228,
"execution_time": null
},
{
"id": "15176a5f",
"group_id": "1",

17750
data/log.txt

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,980 @@
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
import glob
class CSharpLauncherManager:
def __init__(self, data_path: str):
self.data_path = data_path
self.launcher_config_path = os.path.join(data_path, "csharp_launcher_projects.json")
self.favorites_path = os.path.join(data_path, "csharp_launcher_favorites.json")
self.script_metadata_path = os.path.join(data_path, "csharp_launcher_metadata.json")
# Procesos en ejecución para C#
self.running_processes = {}
self.process_lock = threading.Lock()
# Inicializar archivos si no existen
self._initialize_files()
def _initialize_files(self):
"""Crear archivos de configuración por defecto si no existen"""
# Inicializar csharp_launcher_projects.json
if not os.path.exists(self.launcher_config_path):
default_config = {
"version": "1.0",
"projects": [],
"categories": {
"Aplicaciones": {
"color": "#3B82F6",
"icon": "🖥️",
"subcategories": ["Desktop", "Consola", "WPF"]
},
"Herramientas": {
"color": "#10B981",
"icon": "🔧",
"subcategories": ["Utilidades", "Automatización", "Sistema"]
},
"Análisis": {
"color": "#8B5CF6",
"icon": "📊",
"subcategories": ["Datos", "Reportes", "Business Intelligence"]
},
"Desarrollo": {
"color": "#F59E0B",
"icon": "💻",
"subcategories": ["Testing", "Build Tools", "DevOps"]
},
"APIs": {
"color": "#EF4444",
"icon": "🌐",
"subcategories": ["REST", "GraphQL", "Microservicios"]
},
"Otros": {
"color": "#6B7280",
"icon": "📁",
"subcategories": ["Misceláneos"]
}
},
"settings": {
"default_execution_directory": "project_directory",
"search_debug_first": True,
"show_build_output": True
}
}
with open(self.launcher_config_path, 'w', encoding='utf-8') as f:
json.dump(default_config, f, indent=2, ensure_ascii=False)
# Inicializar csharp_launcher_favorites.json
if not os.path.exists(self.favorites_path):
default_favorites = {"favorites": []}
with open(self.favorites_path, 'w', encoding='utf-8') as f:
json.dump(default_favorites, f, indent=2, ensure_ascii=False)
# Inicializar csharp_launcher_metadata.json
if not os.path.exists(self.script_metadata_path):
default_metadata = {
"version": "1.0",
"executable_metadata": {}
}
with open(self.script_metadata_path, 'w', encoding='utf-8') as f:
json.dump(default_metadata, f, indent=2, ensure_ascii=False)
def get_csharp_projects(self) -> List[Dict[str, Any]]:
"""Obtener todos los proyectos C#"""
try:
with open(self.launcher_config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
return config.get("projects", [])
except Exception as e:
print(f"Error loading C# projects: {e}")
return []
def get_csharp_project(self, project_id: str) -> Optional[Dict[str, Any]]:
"""Obtener un proyecto específico por ID"""
projects = self.get_csharp_projects()
for project in projects:
if project.get("id") == project_id:
return project
return None
def add_csharp_project(self, project_data: Dict[str, Any]) -> Dict[str, str]:
"""Agregar nuevo proyecto C#"""
try:
# Validar datos requeridos
required_fields = ["name", "directory"]
for field in required_fields:
if not project_data.get(field):
return {"status": "error", "message": f"Campo requerido: {field}"}
# Validar que el directorio existe
if not os.path.isdir(project_data["directory"]):
return {"status": "error", "message": "El directorio especificado no existe"}
# Generar ID único si no se proporciona
if not project_data.get("id"):
project_data["id"] = str(uuid.uuid4())[:8]
# Verificar que el ID no exista
if self.get_csharp_project(project_data["id"]):
return {"status": "error", "message": "Ya existe un proyecto con este ID"}
# Agregar campos por defecto
current_time = datetime.now().isoformat() + "Z"
project_data.setdefault("description", "")
project_data.setdefault("category", "Otros")
project_data.setdefault("version", "1.0")
project_data.setdefault("author", "")
project_data.setdefault("tags", [])
project_data.setdefault("dotnet_version", "") # Versión de .NET
project_data.setdefault("created_date", current_time)
project_data["updated_date"] = current_time
# Cargar configuración y agregar proyecto
with open(self.launcher_config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
config["projects"].append(project_data)
with open(self.launcher_config_path, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False)
return {"status": "success", "message": "Proyecto agregado exitosamente"}
except Exception as e:
return {"status": "error", "message": f"Error agregando proyecto: {str(e)}"}
def update_csharp_project(self, project_id: str, project_data: Dict[str, Any]) -> Dict[str, str]:
"""Actualizar proyecto existente"""
try:
with open(self.launcher_config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
# Buscar y actualizar el proyecto
project_found = False
for i, project in enumerate(config["projects"]):
if project["id"] == project_id:
# Mantener ID y fechas de creación
project_data["id"] = project_id
project_data["created_date"] = project.get("created_date", datetime.now().isoformat() + "Z")
project_data["updated_date"] = datetime.now().isoformat() + "Z"
config["projects"][i] = project_data
project_found = True
break
if not project_found:
return {"status": "error", "message": "Proyecto no encontrado"}
with open(self.launcher_config_path, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False)
return {"status": "success", "message": "Proyecto actualizado exitosamente"}
except Exception as e:
return {"status": "error", "message": f"Error actualizando proyecto: {str(e)}"}
def delete_csharp_project(self, project_id: str) -> Dict[str, str]:
"""Eliminar proyecto C#"""
try:
with open(self.launcher_config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
# Filtrar el proyecto a eliminar
original_count = len(config["projects"])
config["projects"] = [p for p in config["projects"] if p["id"] != project_id]
if len(config["projects"]) == original_count:
return {"status": "error", "message": "Proyecto no encontrado"}
with open(self.launcher_config_path, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False)
# Limpiar metadatos y favoritos relacionados
self._cleanup_metadata_for_project(project_id)
self._cleanup_favorites_for_project(project_id)
self._cleanup_favorites_for_project(project_id)
return {"status": "success", "message": "Proyecto eliminado exitosamente"}
except Exception as e:
return {"status": "error", "message": f"Error eliminando proyecto: {str(e)}"}
def execute_csharp_executable(self, project_id: str, exe_name: str, exe_args: List[str],
broadcast_func, working_dir: str = None) -> Dict[str, Any]:
"""Ejecutar un ejecutable C#"""
try:
project = self.get_csharp_project(project_id)
if not project:
return {"status": "error", "message": "Proyecto no encontrado"}
# Buscar el ejecutable
executables = self.get_all_project_executables(project_id)
exe_info = None
for exe in executables:
if exe["filename"] == exe_name:
exe_info = exe
break
if not exe_info:
return {"status": "error", "message": f"Ejecutable '{exe_name}' no encontrado"}
exe_path = exe_info["full_path"]
# Determinar directorio de trabajo
if working_dir and os.path.isdir(working_dir):
work_dir = working_dir
else:
work_dir = os.path.dirname(exe_path)
# Preparar comando
cmd = [exe_path] + exe_args
execution_id = str(uuid.uuid4())[:8]
broadcast_func(f"🚀 Ejecutando: {exe_info['display_name']}")
broadcast_func(f"📁 Directorio: {work_dir}")
broadcast_func(f"⚡ Comando: {' '.join(cmd)}")
broadcast_func("=" * 50)
try:
# Ejecutar el proceso
process = subprocess.Popen(
cmd,
cwd=work_dir,
creationflags=subprocess.CREATE_NEW_CONSOLE if sys.platform == "win32" else 0
)
# Almacenar información del proceso
with self.process_lock:
self.running_processes[process.pid] = {
"project_id": project_id,
"exe_name": exe_name,
"display_name": exe_info['display_name'],
"start_time": datetime.now(),
"process": process
}
broadcast_func(f"✅ Proceso iniciado con PID: {process.pid}")
return {
"status": "success",
"message": f"Ejecutable '{exe_info['display_name']}' iniciado",
"pid": process.pid,
"execution_id": execution_id
}
except subprocess.SubprocessError as e:
return {"status": "error", "message": f"Error ejecutando el proceso: {str(e)}"}
except Exception as e:
return {"status": "error", "message": f"Error inesperado: {str(e)}"}
except Exception as e:
return {"status": "error", "message": f"Error ejecutando ejecutable: {str(e)}"}
def get_favorites(self) -> List[Dict[str, Any]]:
"""Obtener lista de favoritos"""
try:
with open(self.favorites_path, 'r', encoding='utf-8') as f:
data = json.load(f)
return data.get("favorites", [])
except Exception as e:
print(f"Error loading C# favorites: {e}")
return []
def toggle_favorite(self, project_id: str, exe_name: str) -> Dict[str, str]:
"""Agregar o quitar de favoritos"""
try:
with open(self.favorites_path, 'r', encoding='utf-8') as f:
data = json.load(f)
favorites = data.get("favorites", [])
favorite_key = f"{project_id}_{exe_name}"
# Buscar si ya existe
existing_favorite = None
for fav in favorites:
if fav.get("project_id") == project_id and fav.get("exe_name") == exe_name:
existing_favorite = fav
break
if existing_favorite:
# Quitar de favoritos
favorites.remove(existing_favorite)
message = "Removido de favoritos"
is_favorite = False
else:
# Agregar a favoritos
favorites.append({
"id": favorite_key,
"project_id": project_id,
"exe_name": exe_name,
"added_date": datetime.now().isoformat() + "Z"
})
message = "Agregado a favoritos"
is_favorite = True
data["favorites"] = favorites
with open(self.favorites_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return {"status": "success", "message": message, "is_favorite": is_favorite}
except Exception as e:
return {"status": "error", "message": f"Error toggle favorite: {str(e)}"}
def get_categories(self) -> Dict[str, Any]:
"""Obtener categorías disponibles"""
try:
with open(self.launcher_config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
return config.get("categories", {})
except Exception as e:
print(f"Error loading C# categories: {e}")
return {}
def get_running_processes(self) -> List[Dict[str, Any]]:
"""Obtener procesos C# en ejecución"""
processes = []
with self.process_lock:
for pid, info in list(self.running_processes.items()):
try:
# Verificar si el proceso sigue activo
process = info["process"]
if process.poll() is None:
processes.append({
"pid": pid,
"project_id": info["project_id"],
"exe_name": info["exe_name"],
"display_name": info["display_name"],
"start_time": info["start_time"].isoformat() + "Z"
})
else:
# Proceso terminado, remover de la lista
del self.running_processes[pid]
except:
# Error verificando proceso, remover
del self.running_processes[pid]
return processes
def terminate_process(self, pid: int) -> Dict[str, str]:
"""Cerrar un proceso C#"""
try:
with self.process_lock:
if pid in self.running_processes:
process_info = self.running_processes[pid]
process = process_info["process"]
try:
process.terminate()
process.wait(timeout=5)
del self.running_processes[pid]
return {
"status": "success",
"message": f"Proceso {process_info['display_name']} (PID: {pid}) terminado"
}
except subprocess.TimeoutExpired:
process.kill()
del self.running_processes[pid]
return {
"status": "success",
"message": f"Proceso {process_info['display_name']} (PID: {pid}) forzado a cerrar"
}
else:
return {"status": "error", "message": "Proceso no encontrado en ejecución"}
except Exception as e:
return {"status": "error", "message": f"Error terminando proceso: {str(e)}"}
def _load_executable_metadata(self) -> Dict[str, Any]:
"""Cargar metadatos de ejecutables desde archivo"""
try:
with open(self.script_metadata_path, 'r', encoding='utf-8') as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return {"version": "1.0", "executable_metadata": {}}
def get_project_executables(self, project_id: str) -> List[Dict[str, Any]]:
"""Obtener ejecutables de un proyecto específico (solo visibles)"""
project = self.get_csharp_project(project_id)
if not project:
return []
project_dir = project["directory"]
if not os.path.isdir(project_dir):
return []
executables = []
metadata = self._load_executable_metadata()
# Buscar en bin/Release y bin/Debug
search_patterns = [
os.path.join(project_dir, "**/bin/Release/**/*.exe"),
os.path.join(project_dir, "**/bin/Debug/**/*.exe")
]
found_exe_files = set()
for pattern in search_patterns:
for exe_path in glob.glob(pattern, recursive=True):
if os.path.isfile(exe_path):
found_exe_files.add(exe_path)
for exe_path in found_exe_files:
exe_name = os.path.basename(exe_path)
exe_key = f"{project_id}_{exe_name}"
# Obtener metadatos o crear por defecto
exe_metadata = metadata.get("executable_metadata", {}).get(exe_key, {})
# Solo incluir si no está oculto
if not exe_metadata.get("hidden", False):
# Determinar si es Debug o Release
build_type = "Release" if "\\Release\\" in exe_path or "/Release/" in exe_path else "Debug"
executables.append({
"filename": exe_name,
"full_path": exe_path,
"display_name": exe_metadata.get("display_name", exe_name.replace('.exe', '')),
"short_description": exe_metadata.get("short_description", f"Aplicación C# ({build_type})"),
"long_description": exe_metadata.get("long_description", ""),
"build_type": build_type,
"relative_path": os.path.relpath(exe_path, project_dir)
})
# Ordenar por nombre de display
executables.sort(key=lambda x: x['display_name'])
return executables
def get_all_project_executables(self, project_id: str) -> List[Dict[str, Any]]:
"""Obtener TODOS los ejecutables de un proyecto (incluyendo ocultos) para gestión"""
project = self.get_csharp_project(project_id)
if not project:
return []
project_dir = project["directory"]
if not os.path.isdir(project_dir):
return []
executables = []
metadata = self._load_executable_metadata()
# Buscar en bin/Release y bin/Debug
search_patterns = [
os.path.join(project_dir, "**/bin/Release/**/*.exe"),
os.path.join(project_dir, "**/bin/Debug/**/*.exe")
]
found_exe_files = set()
for pattern in search_patterns:
for exe_path in glob.glob(pattern, recursive=True):
if os.path.isfile(exe_path):
found_exe_files.add(exe_path)
for exe_path in found_exe_files:
exe_name = os.path.basename(exe_path)
exe_key = f"{project_id}_{exe_name}"
# Obtener metadatos o crear por defecto
exe_metadata = metadata.get("executable_metadata", {}).get(exe_key, {})
# Determinar si es Debug o Release
build_type = "Release" if "\\Release\\" in exe_path or "/Release/" in exe_path else "Debug"
executables.append({
"filename": exe_name,
"full_path": exe_path,
"display_name": exe_metadata.get("display_name", exe_name.replace('.exe', '')),
"short_description": exe_metadata.get("short_description", f"Aplicación C# ({build_type})"),
"long_description": exe_metadata.get("long_description", ""),
"build_type": build_type,
"relative_path": os.path.relpath(exe_path, project_dir),
"hidden": exe_metadata.get("hidden", False)
})
# Ordenar por nombre de display
executables.sort(key=lambda x: x['display_name'])
return executables
def get_executable_metadata(self, project_id: str, exe_name: str) -> Dict[str, Any]:
"""Obtener metadatos de un ejecutable específico"""
metadata = self._load_executable_metadata()
exe_key = f"{project_id}_{exe_name}"
return metadata.get("executable_metadata", {}).get(exe_key, {
"display_name": exe_name.replace('.exe', ''),
"short_description": "Aplicación C#",
"long_description": "",
"hidden": False
})
def update_executable_metadata(self, project_id: str, exe_name: str, metadata_update: Dict[str, Any]) -> Dict[str, str]:
"""Actualizar metadatos de un ejecutable específico"""
try:
metadata = self._load_executable_metadata()
exe_key = f"{project_id}_{exe_name}"
if "executable_metadata" not in metadata:
metadata["executable_metadata"] = {}
if exe_key not in metadata["executable_metadata"]:
metadata["executable_metadata"][exe_key] = {}
# Actualizar campos
metadata["executable_metadata"][exe_key].update(metadata_update)
self._save_executable_metadata(metadata)
return {"status": "success", "message": "Metadatos actualizados exitosamente"}
except Exception as e:
return {"status": "error", "message": f"Error actualizando metadatos: {str(e)}"}
def execute_csharp_executable(self, project_id: str, exe_name: str, exe_args: List[str],
broadcast_func, working_dir: str = None) -> Dict[str, Any]:
"""Ejecutar un ejecutable C#"""
try:
project = self.get_csharp_project(project_id)
if not project:
return {"status": "error", "message": "Proyecto no encontrado"}
# Buscar el ejecutable
executables = self.get_all_project_executables(project_id)
exe_info = None
for exe in executables:
if exe["filename"] == exe_name:
exe_info = exe
break
if not exe_info:
return {"status": "error", "message": f"Ejecutable '{exe_name}' no encontrado"}
exe_path = exe_info["full_path"]
# Determinar directorio de trabajo
if working_dir and os.path.isdir(working_dir):
work_dir = working_dir
else:
work_dir = os.path.dirname(exe_path)
# Preparar comando
cmd = [exe_path] + exe_args
execution_id = str(uuid.uuid4())[:8]
broadcast_func(f"🚀 Ejecutando: {exe_info['display_name']}")
broadcast_func(f"📁 Directorio: {work_dir}")
broadcast_func(f"⚡ Comando: {' '.join(cmd)}")
broadcast_func("=" * 50)
try:
# Ejecutar el proceso
process = subprocess.Popen(
cmd,
cwd=work_dir,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
universal_newlines=True,
creationflags=subprocess.CREATE_NEW_CONSOLE if sys.platform == "win32" else 0
)
# Almacenar información del proceso
with self.process_lock:
self.running_processes[process.pid] = {
"project_id": project_id,
"exe_name": exe_name,
"display_name": exe_info['display_name'],
"start_time": datetime.now(),
"process": process
}
broadcast_func(f"✅ Proceso iniciado con PID: {process.pid}")
# Monitorear salida en hilo separado
def read_output():
try:
for line in iter(process.stdout.readline, ''):
if line.strip():
broadcast_func(line.rstrip())
except Exception as e:
broadcast_func(f"Error leyendo salida: {e}")
finally:
if process.stdout:
process.stdout.close()
output_thread = threading.Thread(target=read_output, daemon=True)
output_thread.start()
# Monitorear finalización en hilo separado
def monitor_completion():
try:
start_time = time.time()
return_code = process.wait()
execution_time = time.time() - start_time
# Limpiar de procesos en ejecución
with self.process_lock:
if process.pid in self.running_processes:
del self.running_processes[process.pid]
if return_code == 0:
broadcast_func(f"✅ Proceso completado exitosamente (PID: {process.pid})")
else:
broadcast_func(f"❌ Proceso terminó con código: {return_code} (PID: {process.pid})")
broadcast_func(f"⏱️ Tiempo de ejecución: {execution_time:.2f} segundos")
broadcast_func("=" * 50)
except Exception as e:
broadcast_func(f"Error monitoreando proceso: {e}")
monitor_thread = threading.Thread(target=monitor_completion, daemon=True)
monitor_thread.start()
return {
"status": "success",
"message": f"Ejecutable '{exe_info['display_name']}' iniciado",
"pid": process.pid,
"execution_id": execution_id
}
except subprocess.SubprocessError as e:
return {"status": "error", "message": f"Error ejecutando el proceso: {str(e)}"}
except Exception as e:
return {"status": "error", "message": f"Error inesperado: {str(e)}"}
except Exception as e:
return {"status": "error", "message": f"Error ejecutando ejecutable: {str(e)}"}
def get_favorites(self) -> List[Dict[str, Any]]:
"""Obtener lista de favoritos"""
try:
with open(self.favorites_path, 'r', encoding='utf-8') as f:
data = json.load(f)
return data.get("favorites", [])
except Exception as e:
print(f"Error loading C# favorites: {e}")
return []
def toggle_favorite(self, project_id: str, exe_name: str) -> Dict[str, str]:
"""Agregar o quitar de favoritos"""
try:
with open(self.favorites_path, 'r', encoding='utf-8') as f:
data = json.load(f)
favorites = data.get("favorites", [])
favorite_key = f"{project_id}_{exe_name}"
# Buscar si ya existe
existing_favorite = None
for fav in favorites:
if fav.get("project_id") == project_id and fav.get("exe_name") == exe_name:
existing_favorite = fav
break
if existing_favorite:
# Quitar de favoritos
favorites.remove(existing_favorite)
message = "Removido de favoritos"
is_favorite = False
else:
# Agregar a favoritos
favorites.append({
"id": favorite_key,
"project_id": project_id,
"exe_name": exe_name,
"added_date": datetime.now().isoformat() + "Z"
})
message = "Agregado a favoritos"
is_favorite = True
data["favorites"] = favorites
with open(self.favorites_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return {
"status": "success",
"message": message,
"is_favorite": is_favorite
}
except Exception as e:
return {"status": "error", "message": f"Error gestionando favorito: {str(e)}"}
def get_categories(self) -> Dict[str, Any]:
"""Obtener categorías disponibles"""
try:
with open(self.launcher_config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
return config.get("categories", {})
except Exception as e:
print(f"Error loading C# categories: {e}")
return {}
def get_running_processes(self) -> List[Dict[str, Any]]:
"""Obtener procesos C# en ejecución"""
with self.process_lock:
processes = []
for pid, info in self.running_processes.items():
try:
# Verificar si el proceso sigue activo
if info["process"].poll() is None:
processes.append({
"pid": pid,
"project_id": info["project_id"],
"exe_name": info["exe_name"],
"display_name": info["display_name"],
"start_time": info["start_time"].isoformat()
})
else:
# Proceso terminado, remover de la lista
del self.running_processes[pid]
except:
# Error verificando proceso, remover
del self.running_processes[pid]
return processes
def terminate_process(self, pid: int) -> Dict[str, str]:
"""Cerrar un proceso C#"""
try:
with self.process_lock:
if pid in self.running_processes:
process_info = self.running_processes[pid]
process = process_info["process"]
try:
process.terminate()
process.wait(timeout=5)
del self.running_processes[pid]
return {
"status": "success",
"message": f"Proceso {process_info['display_name']} (PID: {pid}) terminado"
}
except subprocess.TimeoutExpired:
process.kill()
del self.running_processes[pid]
return {
"status": "success",
"message": f"Proceso {process_info['display_name']} (PID: {pid}) forzado a cerrar"
}
else:
return {"status": "error", "message": "Proceso no encontrado en ejecución"}
except Exception as e:
return {"status": "error", "message": f"Error terminando proceso: {str(e)}"}
def _load_executable_metadata(self) -> Dict[str, Any]:
"""Cargar metadatos de ejecutables desde archivo"""
try:
with open(self.script_metadata_path, 'r', encoding='utf-8') as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return {"version": "1.0", "executable_metadata": {}}
def _save_executable_metadata(self, metadata: Dict[str, Any]):
"""Guardar metadatos de ejecutables a archivo"""
try:
with open(self.script_metadata_path, 'w', encoding='utf-8') as f:
json.dump(metadata, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Error saving C# executable metadata: {e}")
def _cleanup_metadata_for_project(self, project_id: str):
"""Limpiar metadatos de un proyecto eliminado"""
try:
metadata = self._load_executable_metadata()
if "executable_metadata" in metadata:
# Remover metadatos que empiecen con project_id_
keys_to_remove = [key for key in metadata["executable_metadata"].keys()
if key.startswith(f"{project_id}_")]
for key in keys_to_remove:
del metadata["executable_metadata"][key]
self._save_executable_metadata(metadata)
except Exception as e:
print(f"Error cleaning metadata for project {project_id}: {e}")
def _cleanup_favorites_for_project(self, project_id: str):
"""Limpiar favoritos de un proyecto eliminado"""
try:
with open(self.favorites_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# Filtrar favoritos que no sean del proyecto eliminado
data["favorites"] = [fav for fav in data.get("favorites", [])
if fav.get("project_id") != project_id]
with open(self.favorites_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Error cleaning favorites for project {project_id}: {e}")
def get_executable_arguments(self, project_id: str, exe_name: str) -> List[Dict[str, str]]:
"""Obtener argumentos predefinidos de un ejecutable"""
try:
metadata = self._load_executable_metadata()
exe_key = f"{project_id}_{exe_name}"
if "executable_metadata" in metadata and exe_key in metadata["executable_metadata"]:
return metadata["executable_metadata"][exe_key].get("arguments", [])
return []
except Exception as e:
print(f"Error loading executable arguments: {e}")
return []
def update_executable_arguments(self, project_id: str, exe_name: str, arguments: List[Dict[str, str]]) -> Dict[str, str]:
"""Actualizar argumentos predefinidos de un ejecutable"""
try:
metadata = self._load_executable_metadata()
exe_key = f"{project_id}_{exe_name}"
# Crear estructura si no existe
if "executable_metadata" not in metadata:
metadata["executable_metadata"] = {}
if exe_key not in metadata["executable_metadata"]:
metadata["executable_metadata"][exe_key] = {}
# Validar formato de argumentos
for arg in arguments:
if not isinstance(arg, dict) or "description" not in arg or "arguments" not in arg:
return {"status": "error", "message": "Formato de argumentos inválido. Debe ser [{'description': '...', 'arguments': '...'}]"}
# Actualizar argumentos
metadata["executable_metadata"][exe_key]["arguments"] = arguments
metadata["executable_metadata"][exe_key]["updated_date"] = datetime.now().isoformat() + "Z"
self._save_executable_metadata(metadata)
return {"status": "success", "message": "Argumentos actualizados exitosamente"}
except Exception as e:
return {"status": "error", "message": f"Error actualizando argumentos: {str(e)}"}
def get_all_project_executables(self, project_id: str) -> List[Dict[str, Any]]:
"""Obtener TODOS los ejecutables de un proyecto (incluyendo ocultos) para gestión"""
project = self.get_csharp_project(project_id)
if not project:
return []
project_dir = project["directory"]
if not os.path.isdir(project_dir):
return []
executables = []
metadata = self._load_executable_metadata()
# Buscar en bin/Release y bin/Debug
search_patterns = [
os.path.join(project_dir, "**/bin/Release/**/*.exe"),
os.path.join(project_dir, "**/bin/Debug/**/*.exe")
]
found_exe_files = set()
for pattern in search_patterns:
for exe_path in glob.glob(pattern, recursive=True):
if os.path.isfile(exe_path):
found_exe_files.add(exe_path)
for exe_path in found_exe_files:
exe_name = os.path.basename(exe_path)
exe_key = f"{project_id}_{exe_name}"
# Obtener metadatos o crear por defecto
exe_metadata = metadata.get("executable_metadata", {}).get(exe_key, {})
# Determinar si es Debug o Release
build_type = "Release" if "\\Release\\" in exe_path or "/Release/" in exe_path else "Debug"
executables.append({
"filename": exe_name,
"full_path": exe_path,
"display_name": exe_metadata.get("display_name", exe_name.replace('.exe', '')),
"short_description": exe_metadata.get("short_description", f"Aplicación C# ({build_type})"),
"long_description": exe_metadata.get("long_description", ""),
"build_type": build_type,
"relative_path": os.path.relpath(exe_path, project_dir),
"hidden": exe_metadata.get("hidden", False)
})
# Ordenar por nombre de display
executables.sort(key=lambda x: x['display_name'])
return executables
def get_executable_metadata(self, project_id: str, exe_name: str) -> Dict[str, Any]:
"""Obtener metadatos de un ejecutable específico"""
metadata = self._load_executable_metadata()
exe_key = f"{project_id}_{exe_name}"
return metadata.get("executable_metadata", {}).get(exe_key, {
"display_name": exe_name.replace('.exe', ''),
"short_description": "Aplicación C#",
"long_description": "",
"hidden": False
})
def update_executable_metadata(self, project_id: str, exe_name: str, metadata_update: Dict[str, Any]) -> Dict[str, str]:
"""Actualizar metadatos de un ejecutable específico"""
try:
metadata = self._load_executable_metadata()
exe_key = f"{project_id}_{exe_name}"
if "executable_metadata" not in metadata:
metadata["executable_metadata"] = {}
if exe_key not in metadata["executable_metadata"]:
metadata["executable_metadata"][exe_key] = {}
# Actualizar campos
metadata["executable_metadata"][exe_key].update(metadata_update)
self._save_executable_metadata(metadata)
return {"status": "success", "message": "Metadatos actualizados exitosamente"}
except Exception as e:
return {"status": "error", "message": f"Error actualizando metadatos: {str(e)}"}
def _save_executable_metadata(self, metadata: Dict[str, Any]):
"""Guardar metadatos de ejecutables a archivo"""
try:
with open(self.script_metadata_path, 'w', encoding='utf-8') as f:
json.dump(metadata, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Error saving C# executable metadata: {e}")
def _cleanup_metadata_for_project(self, project_id: str):
"""Limpiar metadatos de un proyecto eliminado"""
try:
metadata = self._load_executable_metadata()
if "executable_metadata" in metadata:
# Remover metadatos que empiecen con project_id_
keys_to_remove = [key for key in metadata["executable_metadata"].keys()
if key.startswith(f"{project_id}_")]
for key in keys_to_remove:
del metadata["executable_metadata"][key]
self._save_executable_metadata(metadata)
except Exception as e:
print(f"Error cleaning metadata for project {project_id}: {e}")
def _cleanup_favorites_for_project(self, project_id: str):
"""Limpiar favoritos de un proyecto eliminado"""
try:
with open(self.favorites_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# Filtrar favoritos que no sean del proyecto eliminado
data["favorites"] = [fav for fav in data.get("favorites", [])
if fav.get("project_id") != project_id]
with open(self.favorites_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Error cleaning favorites for project {project_id}: {e}")

1537
static/js/csharp_launcher.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1369,6 +1369,17 @@ function switchTab(tabName) {
window.launcherManager = new LauncherManager();
window.launcherManager.init();
}
// Inicializar C# launcher si es la primera vez
if (tabName === 'csharp') {
if (!window.csharpLauncherManager) {
console.error('csharpLauncherManager not found! Make sure csharp_launcher.js is loaded.');
return;
}
if (!window.csharpLauncherManager.initialized) {
window.csharpLauncherManager.init();
}
}
}
// Funciones para modales

View File

@ -114,7 +114,18 @@
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z">
</path>
</svg>
Launcher GUI
Launcher GUI (Python)
</span>
</button>
<button id="csharp-tab" onclick="switchTab('csharp')"
class="tab-button py-2 px-1 border-b-2 font-medium text-sm">
<span class="flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0-1.125.504-1.125 1.125V11.25a9 9 0 00-9-9z">
</path>
</svg>
Launcher C#
</span>
</button>
</nav>
@ -232,7 +243,7 @@
<!-- Launcher Controls -->
<div class="mb-6 bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Launcher GUI - Scripts Independientes</h2>
<h2 class="text-xl font-bold">Launcher GUI - Scripts Python Independientes</h2>
<button onclick="openGroupEditor()"
class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
Gestionar Grupos
@ -367,6 +378,248 @@
</div>
</div>
<!-- Tab Content: Launcher C# -->
<div id="csharp-content" class="tab-content hidden">
<!-- C# Project Controls -->
<div class="mb-6 bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Launcher C# - Proyectos Compilados</h2>
<button onclick="openCSharpProjectEditor()"
class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600"
onmousedown="if(!window.openCSharpProjectEditor) alert('Launcher C# cargando...')">
Gestionar Proyectos
</button>
</div>
<!-- Project Selector -->
<div class="mb-4">
<label class="block text-sm font-medium mb-2">Seleccionar Proyecto C#</label>
<div class="flex gap-2">
<div class="relative flex-1">
<select id="csharp-project-select" class="w-full p-3 border rounded-lg pl-12"
onchange="loadCSharpExecutables()">
<option value="">-- Seleccionar Proyecto --</option>
</select>
<div class="absolute left-3 top-1/2 transform -translate-y-1/2">
<div id="selected-csharp-project-icon"
class="w-6 h-6 bg-gray-200 rounded flex items-center justify-center text-sm">🗂️
</div>
</div>
</div>
<button onclick="openCSharpProjectInEditor('cursor')"
class="bg-purple-500 text-white px-4 py-3 rounded-lg hover:bg-purple-600"
id="cursor-csharp-btn" style="display: none;" title="Abrir proyecto en Cursor">
<img src="{{ url_for('static', filename='icons/cursor.png') }}" class="w-5 h-5"
alt="Cursor Icon">
</button>
<button onclick="openCSharpProjectInEditor('vs2022')"
class="bg-purple-500 text-white px-4 py-3 rounded-lg hover:bg-purple-600"
id="vs2022-csharp-btn" style="display: none;" title="Abrir proyecto en Visual Studio 2022">
💜
</button>
<button onclick="openCSharpProjectFolder()"
class="bg-green-500 text-white px-4 py-3 rounded-lg hover:bg-green-600"
id="folder-csharp-btn" style="display: none;" title="Abrir carpeta del proyecto">
📁
</button>
<button onclick="copyCSharpProjectPath()"
class="bg-gray-500 text-white px-4 py-3 rounded-lg hover:bg-gray-600"
id="copy-path-csharp-btn" style="display: none;" title="Copiar path del proyecto">
📋
</button>
</div>
</div>
<!-- Category Filter -->
<div class="mb-4">
<h3 class="text-sm font-medium mb-2">Filtrar por Categoría</h3>
<div class="flex flex-wrap gap-2">
<button class="csharp-category-btn active px-3 py-1 rounded-full text-sm border"
data-category="all" onclick="filterCSharpByCategory('all')">
Todas
</button>
<button class="csharp-category-btn px-3 py-1 rounded-full text-sm border"
data-category="Aplicaciones" onclick="filterCSharpByCategory('Aplicaciones')">
🖥️ Aplicaciones
</button>
<button class="csharp-category-btn px-3 py-1 rounded-full text-sm border"
data-category="Herramientas" onclick="filterCSharpByCategory('Herramientas')">
🔧 Herramientas
</button>
<button class="csharp-category-btn px-3 py-1 rounded-full text-sm border" data-category="APIs"
onclick="filterCSharpByCategory('APIs')">
🌐 APIs
</button>
</div>
</div>
</div>
<!-- C# Favorites Panel -->
<div id="csharp-favorites-panel" class="mb-6 bg-blue-50 border border-blue-200 rounded-lg p-4">
<div class="flex items-center justify-between mb-3">
<h3 class="text-lg font-semibold text-blue-800">
⭐ Ejecutables Favoritos
</h3>
<span class="text-sm text-blue-600" id="csharp-favorites-count">
0 favoritos
</span>
</div>
<div id="csharp-favorites-list" class="space-y-2">
<!-- Lista dinámica de favoritos C# -->
</div>
</div>
<!-- C# Executables Grid -->
<div class="mb-6 bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Ejecutables Disponibles</h2>
<button onclick="openCSharpExecutableManager()"
class="bg-purple-500 text-white px-4 py-2 rounded hover:bg-purple-600"
id="manage-csharp-executables-btn" style="display: none;">
Gestionar Ejecutables
</button>
</div>
<div id="csharp-executables-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Executable cards dinámicos -->
</div>
</div>
<!-- Running Processes Panel -->
<div class="mb-6 bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">🔄 Procesos en Ejecución</h3>
<button onclick="refreshCSharpProcesses()" class="text-blue-500 hover:text-blue-700 text-sm">
Actualizar
</button>
</div>
<div id="csharp-running-processes" class="space-y-2">
<!-- Lista dinámica de procesos -->
</div>
</div>
</div>
<!-- Modal: Editor de Proyectos C# -->
<div id="csharp-project-editor-modal"
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-lg shadow-xl w-full max-w-6xl mx-4 max-h-[90vh] overflow-hidden">
<div class="flex justify-between items-center p-6 border-b">
<h2 class="text-xl font-bold">Gestión de Proyectos C#</h2>
<button onclick="closeCSharpProjectEditor()" class="text-gray-500 hover:text-gray-700">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div class="flex h-[70vh]">
<!-- Lista de Proyectos Existentes -->
<div class="w-1/2 p-6 border-r overflow-y-auto">
<h3 class="text-lg font-semibold mb-4">Proyectos Existentes</h3>
<div id="csharp-existing-projects" class="space-y-3">
<!-- Lista dinámica de proyectos -->
</div>
</div>
<!-- Formulario de Proyecto -->
<div class="w-1/2 p-6 overflow-y-auto">
<h3 class="text-lg font-semibold mb-4">Agregar/Editar Proyecto</h3>
<form id="csharp-project-form" class="space-y-4">
<input type="hidden" id="csharp-project-id">
<!-- Nombre del Proyecto -->
<div>
<label class="block text-sm font-medium mb-1">Nombre del Proyecto *</label>
<input type="text" id="csharp-project-name" class="w-full p-2 border rounded-lg"
placeholder="Mi Aplicación C#" required>
</div>
<!-- Descripción -->
<div>
<label class="block text-sm font-medium mb-1">Descripción</label>
<textarea id="csharp-project-description" class="w-full p-2 border rounded-lg h-20"
placeholder="Descripción del proyecto"></textarea>
</div>
<!-- Directorio -->
<div>
<label class="block text-sm font-medium mb-1">Directorio del Proyecto *</label>
<div class="flex gap-2">
<input type="text" id="csharp-project-directory"
class="flex-1 p-2 border rounded-lg" placeholder="C:\Proyectos\MiApp" required>
<button type="button" onclick="browseCSharpProjectDirectory()"
class="bg-gray-500 text-white px-3 py-2 rounded-lg hover:bg-gray-600">
📁
</button>
</div>
</div>
<!-- Categoría -->
<div>
<label class="block text-sm font-medium mb-1">Categoría</label>
<select id="csharp-project-category" class="w-full p-2 border rounded-lg">
<option value="Aplicaciones">🖥️ Aplicaciones</option>
<option value="Herramientas">🔧 Herramientas</option>
<option value="Análisis">📊 Análisis</option>
<option value="Desarrollo">💻 Desarrollo</option>
<option value="APIs">🌐 APIs</option>
<option value="Otros">📁 Otros</option>
</select>
</div>
<!-- Versión -->
<div>
<label class="block text-sm font-medium mb-1">Versión</label>
<input type="text" id="csharp-project-version" class="w-full p-2 border rounded-lg"
placeholder="1.0" value="1.0">
</div>
<!-- Autor -->
<div>
<label class="block text-sm font-medium mb-1">Autor</label>
<input type="text" id="csharp-project-author" class="w-full p-2 border rounded-lg"
placeholder="Nombre del desarrollador">
</div>
<!-- Versión .NET -->
<div>
<label class="block text-sm font-medium mb-1">Versión .NET</label>
<input type="text" id="csharp-project-dotnet-version"
class="w-full p-2 border rounded-lg"
placeholder="6.0, Framework 4.8, Core 3.1, etc.">
</div>
<!-- Tags -->
<div>
<label class="block text-sm font-medium mb-1">Tags</label>
<input type="text" id="csharp-project-tags" class="w-full p-2 border rounded-lg"
placeholder="winforms, wpf, console, api (separados por comas)">
</div>
<!-- Botones -->
<div class="flex justify-between pt-4">
<button type="button" onclick="deleteCSharpProject()"
class="bg-red-500 text-white px-4 py-2 rounded-lg hover:bg-red-600"
id="delete-csharp-project-btn">
Eliminar Proyecto
</button>
<div class="space-x-2">
<button type="button" onclick="closeCSharpProjectEditor()"
class="bg-gray-500 text-white px-4 py-2 rounded-lg hover:bg-gray-600">
Cancelar
</button>
<button type="button" onclick="saveCSharpProject()"
class="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600">
Guardar Proyecto
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Logs (común para ambos sistemas) -->
<div class="bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
@ -780,9 +1033,40 @@
</div>
</div>
<!-- C# Executable Manager Modal -->
<div id="csharp-executable-manager" 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-center">
<h3 class="text-lg font-semibold">Gestionar Ejecutables C#</h3>
<button onclick="closeCSharpExecutableManager()"
class="text-gray-500 hover:text-gray-700 text-2xl">&times;</button>
</div>
<p class="text-sm text-gray-600 mt-1">Edita la visibilidad, descripciones y argumentos de los
ejecutables</p>
</div>
<div class="p-6 overflow-y-auto max-h-[75vh]">
<div id="csharp-executable-list">
<!-- Lista dinámica de ejecutables -->
</div>
</div>
<div class="p-4 border-t bg-gray-50 flex justify-end">
<button onclick="closeCSharpExecutableManager()"
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 src="{{ url_for('static', filename='js/csharp_launcher.js') }}" defer></script>
<script>
// Inicializar markdown-it globalmente
window.markdownit = window.markdownit || markdownit;