Compare commits
12 Commits
734e6637bc
...
b74db36cf9
Author | SHA1 | Date |
---|---|---|
|
b74db36cf9 | |
|
26bc892243 | |
|
99e3628955 | |
|
f57d0f21dc | |
|
5da7dcad06 | |
|
205e1f4c8d | |
|
c597eaa28f | |
|
95eb1bc62f | |
|
e3eb2fb9e5 | |
|
5be80138c5 | |
|
7ab11a94ce | |
|
bf30b2db52 |
|
@ -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
247
app.py
|
@ -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({
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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
|
@ -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 ---
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
]
|
||||
|
|
|
@ -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.")
|
||||
|
||||
|
|
|
@ -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.
|
|
@ -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
|
||||
}
|
||||
}
|
||||
```
|
|
@ -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__/
|
|
@ -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
|
@ -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
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
]
|
||||
|
|
|
@ -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
17750
data/log.txt
File diff suppressed because it is too large
Load Diff
|
@ -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}")
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
|
|
|
@ -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">×</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;
|
||||
|
|
Loading…
Reference in New Issue