Compare commits

...

3 Commits

Author SHA1 Message Date
Miguel ea35ae1211 Implementación de la API de Launcher GUI
- Se agregó el manejo de grupos de lanzadores, permitiendo obtener, crear, actualizar y eliminar grupos a través de nuevas rutas API.
- Se implementaron funciones para gestionar scripts de lanzadores, incluyendo la obtención de scripts por grupo y la ejecución de scripts GUI con argumentos.
- Se añadió la gestión de favoritos y el historial de lanzadores, mejorando la experiencia del usuario al interactuar con la interfaz.
- Se realizaron ajustes en el archivo `index.html` para incluir nuevas secciones y mejorar la navegación entre configuraciones y lanzadores.
2025-06-03 11:47:57 +02:00
Miguel a3618246b7 Iniciando agregado de launcher de scripts con GUI 2025-06-03 11:35:37 +02:00
Miguel c0a0a5e088 Actualización de logs y configuración para scripts de exportación en TIA Portal
- Se modificaron los logs de ejecución para reflejar nuevas fechas y duraciones en los scripts x1, x2, x4 y x7.
- Se ajustó el directorio de trabajo en `script_config.json` y `work_dir.json` para apuntar a la nueva ubicación de los archivos.
- Se eliminaron archivos obsoletos como `readme.md` y `siemens_tia_scripting.md`.
- Se mejoró la documentación y se añadieron nuevas descripciones para los scripts en `scripts_description.json`.
2025-06-03 11:04:04 +02:00
42 changed files with 89541 additions and 20568 deletions

1055
adicion_launcher4GUI.md Normal file

File diff suppressed because it is too large Load Diff

149
app.py
View File

@ -1,6 +1,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
import os
import json # Added import
from datetime import datetime
@ -22,6 +23,9 @@ app = Flask(
sock = Sock(app)
config_manager = ConfigurationManager()
# Inicializar launcher manager
launcher_manager = LauncherManager(config_manager.data_path)
# Lista global para mantener las conexiones WebSocket activas
websocket_connections = set()
@ -509,6 +513,151 @@ def exit_application(icon, item):
tray_icon.stop()
# === LAUNCHER GUI APIs ===
@app.route("/api/launcher-groups", methods=["GET", "POST"])
def handle_launcher_groups():
"""Gestionar grupos de launcher (GET: obtener, POST: crear)"""
if request.method == "GET":
try:
groups = launcher_manager.get_launcher_groups()
return jsonify(groups)
except Exception as e:
return jsonify({"error": str(e)}), 500
else: # POST
try:
data = request.json
result = launcher_manager.add_launcher_group(data)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/launcher-groups/<group_id>", methods=["GET", "PUT", "DELETE"])
def handle_launcher_group(group_id):
"""Gestionar grupo específico (GET: obtener, PUT: actualizar, DELETE: eliminar)"""
if request.method == "GET":
try:
group = launcher_manager.get_launcher_group(group_id)
if not group:
return jsonify({"error": "Group not found"}), 404
return jsonify(group)
except Exception as e:
return jsonify({"error": str(e)}), 500
elif request.method == "PUT":
try:
data = request.json
result = launcher_manager.update_launcher_group(group_id, data)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
else: # DELETE
try:
result = launcher_manager.delete_launcher_group(group_id)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/launcher-scripts/<group_id>")
def get_launcher_scripts(group_id):
"""Obtener scripts de un grupo del launcher"""
try:
scripts = launcher_manager.get_group_scripts(group_id)
return jsonify(scripts)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/execute-gui-script", methods=["POST"])
def execute_gui_script():
"""Ejecutar script GUI con argumentos opcionales"""
try:
data = request.json
group_id = data["group_id"]
script_name = data["script_name"]
script_args = data.get("args", [])
result = launcher_manager.execute_gui_script(
group_id, script_name, script_args, broadcast_message
)
return jsonify(result)
except Exception as e:
error_msg = f"Error ejecutando script GUI: {str(e)}"
broadcast_message(error_msg)
return jsonify({"error": error_msg}), 500
@app.route("/api/launcher-favorites", methods=["GET", "POST"])
def handle_launcher_favorites():
"""Gestionar favoritos del launcher"""
if request.method == "GET":
try:
favorites = launcher_manager.get_favorites()
return jsonify({"favorites": favorites})
except Exception as e:
return jsonify({"error": str(e)}), 500
else: # POST
try:
data = request.json
group_id = data["group_id"]
script_name = data["script_name"]
result = launcher_manager.toggle_favorite(group_id, script_name)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/launcher-history", methods=["GET", "DELETE"])
def handle_launcher_history():
"""Gestionar historial del launcher"""
if request.method == "GET":
try:
history = launcher_manager.get_history()
return jsonify({"history": history})
except Exception as e:
return jsonify({"error": str(e)}), 500
else: # DELETE
try:
result = launcher_manager.clear_history()
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/launcher-categories")
def get_launcher_categories():
"""Obtener categorías disponibles del launcher"""
try:
categories = launcher_manager.get_categories()
return jsonify(categories)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/group-icon/<launcher_type>/<group_id>")
def get_group_icon(launcher_type, group_id):
"""Obtener icono de un grupo (config o launcher)"""
try:
if launcher_type == "launcher":
group = launcher_manager.get_launcher_group(group_id)
if not group:
return jsonify({"error": "Group not found"}), 404
icon_path = os.path.join(group["directory"], "icon.ico")
if os.path.exists(icon_path):
from flask import send_file
return send_file(icon_path, mimetype='image/x-icon')
elif launcher_type == "config":
group_path = os.path.join(config_manager.script_groups_path, group_id)
icon_path = os.path.join(group_path, "icon.ico")
if os.path.exists(icon_path):
from flask import send_file
return send_file(icon_path, mimetype='image/x-icon')
# Icono por defecto - devolver datos para que el frontend genere el icono
return jsonify({"type": "default", "icon": "📁"})
except Exception as e:
return jsonify({"error": str(e)}), 500
# === FIN LAUNCHER GUI APIs ===
if __name__ == "__main__":
# --- Start Flask in a background thread ---
flask_thread = threading.Thread(target=run_flask, daemon=True)

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,39 @@
import os
import sys
# Use TIA Scripting via file import (Only if TIA Scripting is not installed as package)
# need to set a global environment variable “TIA_SCRIPTING” with path containing TIA Scripting binaries
if os.getenv('TIA_SCRIPTING') == None:
# if TIA_SCRIPTING global environment variable is not set
# set local variable with the path to TIA Scripting binaries
tia_scripting_directory = "C:\\your\\path\\to\\tia-scripting-python"
sys.path.append(tia_scripting_directory)
else:
# TIA_SCRIPTING global environment variable is set and will be used for import
sys.path.append(os.getenv('TIA_SCRIPTING'))
try:
# import TIA Scripting binaries
# if TIA Scripting is installed as package, global environment variable will be ignored
import siemens_tia_scripting as ts
except ImportError:
# you will run into ImportError also if you are using Python version which is not 3.12.X
print("siemens_tia_scripting could not be found")
sys.exit(0)
portal_mode_ui = ts.Enums.PortalMode.AnyUserInterface
# need to specify target version e.g. "17.0", requires that TIA portal V17 is installed
version = "19.0"
portal = ts.attach_portal(portal_mode = portal_mode_ui, version = version)
project = portal.get_project()
plcs = project.get_plcs()
for plc in plcs:
print(plc.get_name())
plcs[0].open_device_editor()
sys.exit(0)

View File

@ -0,0 +1,114 @@
import os
import sys
# Use TIA Scripting via file import (Only if TIA Scripting is not installed as package)
# need to set a global environment variable “TIA_SCRIPTING” with path containing TIA Scripting binaries
if os.getenv('TIA_SCRIPTING') == None:
# if TIA_SCRIPTING global environment variable is not set
# set local variable with the path to TIA Scripting binaries
tia_scripting_directory = "C:\\your\\path\\to\\tia-scripting-python"
sys.path.append(tia_scripting_directory)
else:
# TIA_SCRIPTING global environment variable is set and will be used for import
sys.path.append(os.getenv('TIA_SCRIPTING'))
try:
# import TIA Scripting binaries
# if TIA Scripting is installed as package, global environment variable will be ignored
import siemens_tia_scripting as ts
except ImportError:
# you will run into ImportError also if you are using Python version which is not 3.12.X
print("siemens_tia_scripting could not be found")
sys.exit(0)
portal_mode_ui = True
keep_tia_portal = False
# example values for directories
project_file_path = "D:\\TiaScripting\\PLC_ExportAll\\PLC_ExportAll.ap19"
export_target_dir = "D:\\TiaScripting\\Export"
keep_folder_structure = True
# need to specify target version e.g. "17.0", requires that TIA portal V17 is installed
version = "19.0"
export_options = ts.Enums.ExportOptions.WithDefaults
export_format = ts.Enums.ExportFormats.SimaticML
if portal_mode_ui == True:
portal = ts.open_portal(ts.Enums.PortalMode.WithGraphicalUserInterface, version = version)
print("- open/attach tia portal with ui")
else:
portal = ts.open_portal(ts.Enums.PortalMode.WithoutGraphicalUserInterface, version = version)
print("- open/attach tia portal without ui")
project = portal.open_project(project_file_path = project_file_path, server_project_view = False)
print("- open project")
plcs = project.get_plcs()
for plc in plcs:
print(plc.get_name())
for tag_table in plc.get_plc_tag_tables():
tag_table.export(target_directory_path = export_target_dir + "\\PlcTags", export_options = export_options, keep_folder_structure = keep_folder_structure)
for program_block in plc.get_program_blocks():
if program_block.is_consistent() == False:
program_block.compile()
if program_block.is_consistent() == True:
program_block.export(target_directory_path = export_target_dir + "\\ProgramBlocks", export_options = export_options, keep_folder_structure = keep_folder_structure)
if program_block.get_property(name="ProgrammingLanguage") == "SCL":
program_block.export(target_directory_path = export_target_dir + "\\ProgramBlocks", export_options = export_options, export_format = ts.Enums.ExportFormats.ExternalSource, keep_folder_structure = keep_folder_structure)
for system_block in plc.get_system_blocks():
if system_block.is_consistent() == False:
system_block.compile()
if system_block.is_consistent() == True:
system_block.export(target_directory_path = export_target_dir + "\\ProgramBlocks", export_options = export_options)
for udt in plc.get_user_data_types():
if udt.is_consistent() == False:
udt.compile()
if udt.is_consistent() == True:
udt.export(target_directory_path = export_target_dir + "\\PlcDataTypes", export_options = export_options, keep_folder_structure = keep_folder_structure)
udt.export(target_directory_path = export_target_dir + "\\PlcDataTypes", export_options = export_options, keep_folder_structure = True)
for force_table in plc.get_force_tables():
if force_table.is_consistent() == True:
force_table.export(target_directory_path = export_target_dir + "\\WatchAndForceTables", export_options = export_options)
for watch_table in plc.get_watch_tables():
if watch_table.is_consistent() == False:
watch_table.compile()
if watch_table.is_consistent() == True:
watch_table.export(target_directory_path = export_target_dir + "\\WatchAndForceTables", export_options = export_options, keep_folder_structure = keep_folder_structure)
for to in plc.get_technology_objects():
if to.is_consistent() == False:
to.compile()
if to.is_consistent() == True:
to.export(target_directory_path = export_target_dir + "\\TechnologyObjects", export_options = export_options, keep_folder_structure = keep_folder_structure)
for software_unit in plc.get_software_units():
target_dir_su= export_target_dir + "\\SoftwareUnits\\" + software_unit.get_name()
software_unit.export_configuration(target_directory_path = target_dir_su)
for program_block in software_unit.get_program_blocks():
if program_block.is_consistent() == False:
program_block.compile()
if program_block.is_consistent() == True:
program_block.export(target_directory_path = target_dir_su + "\\ProgramBlocks", export_options = export_options, keep_folder_structure = keep_folder_structure)
if program_block.get_property(name="ProgrammingLanguage") == "SCL":
program_block.export(target_directory_path = export_target_dir + "\\ProgramBlocks", export_options = export_options, export_format = ts.Enums.ExportFormats.ExternalSource, keep_folder_structure = keep_folder_structure)
for udt in software_unit.get_user_data_types():
if udt.is_consistent() == False:
udt.compile()
if udt.is_consistent() == True:
udt.export(target_directory_path = target_dir_su+ "\\PlcDataTypes", export_options = export_options, keep_folder_structure = keep_folder_structure)
udt.export(target_directory_path = target_dir_su + "\\PlcDataTypes", export_options = export_options, keep_folder_structure = True)
if keep_tia_portal == "False":
portal.close_portal()
print("- close tia portal")
if keep_tia_portal == "True":
print("- keep tia portal")
sys.exit(0)

View File

@ -0,0 +1,45 @@
import os
import sys
# Use TIA Scripting via file import (Only if TIA Scripting is not installed as package)
# need to set a global environment variable “TIA_SCRIPTING” with path containing TIA Scripting binaries
if os.getenv('TIA_SCRIPTING') == None:
# if TIA_SCRIPTING global environment variable is not set
# set local variable with the path to TIA Scripting binaries
tia_scripting_directory = "C:\\your\\path\\to\\tia-scripting-python"
sys.path.append(tia_scripting_directory)
else:
# TIA_SCRIPTING global environment variable is set and will be used for import
sys.path.append(os.getenv('TIA_SCRIPTING'))
try:
# import TIA Scripting binaries
# if TIA Scripting is installed as package, global environment variable will be ignored
import siemens_tia_scripting as ts
except ImportError:
# you will run into ImportError also if you are using Python version which is not 3.12.X
print("siemens_tia_scripting could not be found")
sys.exit(0)
bundles = ts.get_installed_bundles()
for bundle in bundles:
print("---------------------------------------------------------------------------------------")
print("Bundle ",bundle.get_title(), bundle.get_release())
for product in bundle.get_products():
print("product: ",product.get_name(), "release: ", product.get_release(), "version: ",product.get_version())
print("---------------------------------------------------------------------------------------")
print("------------------------------------------------------------------------------------------------------------------")
print("------------------------------------------------------------------------------------------------------------------")
print("------------------------------------------------------------------------------------------------------------------")
print("------------------------------------------GET INSTALLED PRODUCTS--------------------------------------------------")
print("------------------------------------------------------------------------------------------------------------------")
print("------------------------------------------------------------------------------------------------------------------")
print("------------------------------------------------------------------------------------------------------------------")
products = ts.get_installed_products()
for product in products:
print("product: ",product.get_name(), "version: ",product.get_version())
sys.exit(0)

View File

@ -0,0 +1,59 @@
import os
import sys
# Use TIA Scripting via file import (Only if TIA Scripting is not installed as package)
# need to set a global environment variable “TIA_SCRIPTING” with path containing TIA Scripting binaries
if os.getenv('TIA_SCRIPTING') == None:
# if TIA_SCRIPTING global environment variable is not set
# set local variable with the path to TIA Scripting binaries
tia_scripting_directory = "C:\\your\\path\\to\\tia-scripting-python"
sys.path.append(tia_scripting_directory)
else:
# TIA_SCRIPTING global environment variable is set and will be used for import
sys.path.append(os.getenv('TIA_SCRIPTING'))
try:
# import TIA Scripting binaries
# if TIA Scripting is installed as package, global environment variable will be ignored
import siemens_tia_scripting as ts
except ImportError:
# you will run into ImportError also if you are using Python version which is not 3.12.X
print("siemens_tia_scripting could not be found")
sys.exit(0)
portal_mode_ui = True
keep_tia_portal = False
project_file_path = "D:\\TiaScripting\\PLC_ImportAll\\PLC_ImportAll.ap17"
import_root_dir = "D:\\TiaScripting\\Export"
version = "17.0"
if portal_mode_ui == True:
portal = ts.open_portal(ts.Enums.PortalMode.WithGraphicalUserInterface, version = version)
print("- open/attach tia portal with ui")
else:
portal = ts.open_portal(ts.Enums.PortalMode.WithoutGraphicalUserInterface, version = version)
print("- open/attach tia portal without ui")
project = portal.open_project(project_file_path = project_file_path)
print("- open project")
plcs = project.get_plcs()
for plc in plcs:
try: plc.import_software_units(import_root_directory= import_root_dir + "\\SoftwareUnits")
except: pass
plc.import_data_types(import_root_directory= import_root_dir + "\\PlcDataTypes")
plc.import_technology_objects(import_root_directory= import_root_dir + "\\TechnologyObjects")
plc.import_blocks(import_root_directory= import_root_dir + "\\ProgramBlocks")
plc.import_plc_tags(import_root_directory= import_root_dir + "\\PlcTags")
plc.import_watch_tables(import_root_directory= import_root_dir + "\\WatchAndForceTables")
if keep_tia_portal == "False":
portal.close_portal()
print("- close tia portal")
if keep_tia_portal == "True":
print("- keep tia portal")
sys.exit(0)

View File

@ -0,0 +1,190 @@
"""
Explorador de la API real del paquete siemens_tiaportal_scripting instalado
Descubre qué está realmente disponible
"""
def explore_actual_api():
"""Explore what's actually available in the installed package."""
print("="*70)
print("EXPLORADOR DE LA API REAL INSTALADA")
print("="*70)
try:
import siemens_tiaportal_scripting as ts
print("✅ Paquete importado correctamente")
# Get all available attributes
all_attrs = dir(ts)
public_attrs = [attr for attr in all_attrs if not attr.startswith('_')]
print(f"\n📋 TODOS LOS ATRIBUTOS DISPONIBLES ({len(public_attrs)}):")
for i, attr in enumerate(sorted(public_attrs), 1):
try:
obj = getattr(ts, attr)
obj_type = type(obj).__name__
print(f" {i:2d}. {attr:<25} : {obj_type}")
# If it's a callable, try to get more info
if callable(obj):
try:
doc = obj.__doc__
if doc:
# Show first line of docstring
first_line = doc.split('\n')[0].strip()
if first_line:
print(f" └─ {first_line}")
except:
pass
except Exception as attr_ex:
print(f" {i:2d}. {attr:<25} : Error - {attr_ex}")
# Look for functions that might be entry points
print(f"\n🔍 FUNCIONES DISPONIBLES:")
functions = [attr for attr in public_attrs if callable(getattr(ts, attr, None))]
for func_name in functions:
try:
func = getattr(ts, func_name)
print(f" 📞 {func_name}")
# Try to get function signature
import inspect
try:
sig = inspect.signature(func)
print(f" Signature: {func_name}{sig}")
except:
print(f" Signature: No disponible")
# Get docstring
doc = func.__doc__
if doc:
lines = doc.strip().split('\n')
for line in lines[:3]: # Show first 3 lines
if line.strip():
print(f" Doc: {line.strip()}")
except Exception as func_ex:
print(f" 📞 {func_name}: Error - {func_ex}")
# Look for classes or types
print(f"\n🏗️ CLASES/TIPOS DISPONIBLES:")
types_found = []
for attr in public_attrs:
try:
obj = getattr(ts, attr)
obj_type = type(obj)
# Check if it's a type/class
if str(obj_type) in ['<class \'type\'>', '<class \'ABCMeta\'>'] or hasattr(obj, '__bases__'):
types_found.append(attr)
print(f" 🏷️ {attr}: {obj_type}")
# Show methods of the class
methods = [m for m in dir(obj) if not m.startswith('_') and callable(getattr(obj, m, None))]
if methods:
print(f" Methods: {', '.join(methods[:5])}")
if len(methods) > 5:
print(f" ... and {len(methods)-5} more")
except Exception as type_ex:
continue
if not types_found:
print(" ❌ No se encontraron clases obvias")
# Try to find TIA Portal related functionality
print(f"\n🎯 BÚSQUEDA DE FUNCIONALIDAD TIA PORTAL:")
tia_keywords = ['tia', 'portal', 'open', 'connect', 'project', 'export']
tia_related = []
for attr in public_attrs:
attr_lower = attr.lower()
if any(keyword in attr_lower for keyword in tia_keywords):
tia_related.append(attr)
if tia_related:
print(" Elementos relacionados con TIA Portal:")
for attr in tia_related:
try:
obj = getattr(ts, attr)
print(f"{attr}: {type(obj).__name__}")
except:
print(f"{attr}: Error")
else:
print(" ❌ No se encontraron elementos obvios relacionados con TIA Portal")
# Try some common function names that might exist
print(f"\n🔍 PROBANDO FUNCIONES COMUNES:")
common_functions = [
'open_portal',
'OpenPortal',
'open_project',
'OpenProject',
'connect',
'Connect',
'TiaPortal',
'create_portal',
'get_portal'
]
for func_name in common_functions:
if hasattr(ts, func_name):
print(f"{func_name}: Disponible")
try:
func = getattr(ts, func_name)
print(f" Tipo: {type(func).__name__}")
if callable(func):
try:
sig = inspect.signature(func)
print(f" Signature: {func_name}{sig}")
except:
pass
except:
pass
else:
print(f"{func_name}: No disponible")
# Check the module file for more info
print(f"\n📁 INFORMACIÓN DEL ARCHIVO:")
if hasattr(ts, '__file__'):
print(f" Archivo: {ts.__file__}")
# Try to read some info from the file
import os
file_path = ts.__file__
if os.path.exists(file_path):
file_size = os.path.getsize(file_path)
print(f" Tamaño: {file_size:,} bytes")
# Check if there are any associated files
file_dir = os.path.dirname(file_path)
files_in_dir = os.listdir(file_dir)
related_files = [f for f in files_in_dir if 'siemens' in f.lower() or 'tia' in f.lower()]
if related_files:
print(f" Archivos relacionados:")
for f in related_files[:10]:
print(f" - {f}")
print(f"\n" + "="*70)
print("🎯 ANÁLISIS COMPLETADO")
print("="*70)
return True
except Exception as e:
print(f"❌ Error durante la exploración: {e}")
import traceback
traceback.print_exc()
return False
if __name__ == "__main__":
success = explore_actual_api()
if success:
print(f"\n📋 Basándome en esta información, crearé el exportador correcto")
else:
print(f"\n❌ No se pudo explorar la API")
input("\nPresiona Enter para salir...")

View File

@ -0,0 +1,229 @@
"""
Verificar si las DLLs de TIA Portal V20 están disponibles para pythonnet
"""
import os
import clr
def check_v20_dlls():
"""Check if TIA Portal V20 DLLs are available."""
print("="*70)
print("VERIFICADOR DE DLL TIA PORTAL V20 PARA SIMATIC SD")
print("="*70)
# Possible paths for V20 DLLs
v20_paths = [
r"C:\Program Files\Siemens\Automation\Portal V20\PublicAPI\V20",
r"C:\Program Files (x86)\Siemens\Automation\Portal V20\PublicAPI\V20",
r"C:\Program Files\Siemens\Automation\Portal V20\bin\PublicAPI\V20"
]
print("🔍 Buscando instalación de TIA Portal V20...")
v20_path = None
for path in v20_paths:
if os.path.exists(path):
dll_path = os.path.join(path, "Siemens.Engineering.dll")
if os.path.exists(dll_path):
v20_path = path
print(f"✅ Encontrado TIA Portal V20 en: {path}")
break
else:
print(f"❌ Directorio existe pero falta Siemens.Engineering.dll: {path}")
else:
print(f"❌ Directorio no encontrado: {path}")
if not v20_path:
print("\n❌ TIA Portal V20 no encontrado")
print("\n📋 OPCIONES DISPONIBLES:")
print("1. 🔧 Instalar TIA Portal V20 completo con Openness")
print("2. 🔧 Usar C# en lugar de Python")
print("3. 🔧 Esperar a una versión más nueva del paquete Python")
return False
print(f"\n📁 Explorando DLLs en: {v20_path}")
# List all DLLs
try:
dlls = [f for f in os.listdir(v20_path) if f.endswith('.dll')]
print(f"DLLs encontradas: {len(dlls)}")
for dll in sorted(dlls):
print(f" - {dll}")
# Check for V20 specific DLLs
v20_dll = "Siemens.TiaPortal.OpennessApi20.dll"
if v20_dll in dlls:
print(f"\n✅ ¡{v20_dll} encontrada!")
print(" Esto significa que tienes API V20 disponible")
else:
print(f"\n{v20_dll} NO encontrada")
print(" Solo tienes APIs hasta V19 - sin SIMATIC SD")
# List available API versions
api_dlls = [dll for dll in dlls if "OpennessApi" in dll]
if api_dlls:
print(f" APIs disponibles: {api_dlls}")
except Exception as list_ex:
print(f"❌ Error listando DLLs: {list_ex}")
return False
# Try to load V20 DLLs with pythonnet
print(f"\n🔧 Probando carga con pythonnet...")
try:
import sys
sys.path.append(v20_path)
# Try to load main assembly
clr.AddReference("Siemens.Engineering")
print("✅ Siemens.Engineering cargada")
# Try to import V20-specific classes
try:
from Siemens.Engineering import TiaPortal, TiaPortalMode
print("✅ TiaPortal y TiaPortalMode importados")
# Try to connect
try:
portal = TiaPortal.Open(TiaPortalMode.WithoutUserInterface)
if portal:
print("✅ Portal V20 abierto exitosamente")
# This would be where you test ExportAsDocuments
print("🎯 Portal V20 funcional - SIMATIC SD potencialmente disponible")
portal.Close()
return True
else:
print("❌ No se pudo abrir portal")
except Exception as portal_ex:
print(f"❌ Error abriendo portal: {portal_ex}")
except Exception as import_ex:
print(f"❌ Error importando clases V20: {import_ex}")
except Exception as load_ex:
print(f"❌ Error cargando DLLs con pythonnet: {load_ex}")
return False
def create_c_sharp_alternative():
"""Create a C# script as alternative."""
print(f"\n🔧 CREANDO ALTERNATIVA EN C#...")
csharp_code = '''
using System;
using System.IO;
using Siemens.Engineering;
using Siemens.Engineering.HW.Features;
class SimaticSDExporter
{
static void Main(string[] args)
{
if (args.Length < 2)
{
Console.WriteLine("Uso: SimaticSDExporter.exe <proyecto.ap20> <directorio_export>");
return;
}
string projectPath = args[0];
string exportDir = args[1];
TiaPortal portal = null;
Project project = null;
try
{
// Abrir TIA Portal V20
portal = TiaPortal.Open(TiaPortalMode.WithUserInterface);
Console.WriteLine("✅ TIA Portal V20 abierto");
// Abrir proyecto
project = portal.Projects.Open(projectPath);
Console.WriteLine($"✅ Proyecto abierto: {project.Name}");
// Buscar PLCs
foreach (Device device in project.Devices)
{
foreach (DeviceItem deviceItem in device.DeviceItems)
{
if (deviceItem.GetService<SoftwareContainer>() != null)
{
var sw = deviceItem.GetService<SoftwareContainer>().Software;
Console.WriteLine($"Procesando PLC: {deviceItem.Name}");
// Exportar bloques en SIMATIC SD
foreach (var block in sw.BlockGroup.Blocks)
{
try
{
string blockExportDir = Path.Combine(exportDir, deviceItem.Name, "SIMATIC_SD");
Directory.CreateDirectory(blockExportDir);
// ¡AQUÍ ESTÁ EL MÉTODO SIMATIC SD!
block.ExportAsDocuments(
new DirectoryInfo(blockExportDir),
block.Name
);
Console.WriteLine($"{block.Name} exportado en SIMATIC SD");
}
catch (Exception blockEx)
{
Console.WriteLine($"❌ Error con {block.Name}: {blockEx.Message}");
}
}
}
}
}
Console.WriteLine("🎉 Exportación SIMATIC SD completada");
}
catch (Exception ex)
{
Console.WriteLine($"❌ Error: {ex.Message}");
}
finally
{
project?.Close();
portal?.Close();
}
}
}
'''
# Save C# code to file
csharp_file = "SimaticSDExporter.cs"
with open(csharp_file, 'w', encoding='utf-8') as f:
f.write(csharp_code)
print(f"✅ Código C# guardado en: {csharp_file}")
print(f"\n📋 PARA COMPILAR Y USAR:")
print(f"1. Instalar Visual Studio o .NET SDK")
print(f"2. Agregar referencias a TIA Portal V20 DLLs")
print(f"3. Compilar: csc SimaticSDExporter.cs /r:Siemens.Engineering.dll")
print(f"4. Ejecutar: SimaticSDExporter.exe proyecto.ap20 directorio_export")
if __name__ == "__main__":
has_v20 = check_v20_dlls()
if not has_v20:
create_c_sharp_alternative()
print(f"\n" + "="*70)
print("📋 RESUMEN DE OPCIONES PARA SIMATIC SD:")
print("="*70)
print("❌ Python: API V19 no tiene SIMATIC SD")
print("✅ C#: Acceso directo a ExportAsDocuments")
print("🔧 Requiere: TIA Portal V20 + Visual Studio")
else:
print(f"\n🎉 ¡TIA Portal V20 disponible para Python!")
print("Puedo crear un exportador Python con pythonnet + V20 DLLs")
input("\nPresiona Enter para salir...")

View File

@ -0,0 +1,80 @@
using System;
using System.IO;
using Siemens.Engineering;
using Siemens.Engineering.HW.Features;
class SimaticSDExporter
{
static void Main(string[] args)
{
if (args.Length < 2)
{
Console.WriteLine("Uso: SimaticSDExporter.exe <proyecto.ap20> <directorio_export>");
return;
}
string projectPath = args[0];
string exportDir = args[1];
TiaPortal portal = null;
Project project = null;
try
{
// Abrir TIA Portal V20
portal = TiaPortal.Open(TiaPortalMode.WithUserInterface);
Console.WriteLine("✅ TIA Portal V20 abierto");
// Abrir proyecto
project = portal.Projects.Open(projectPath);
Console.WriteLine($"✅ Proyecto abierto: {project.Name}");
// Buscar PLCs
foreach (Device device in project.Devices)
{
foreach (DeviceItem deviceItem in device.DeviceItems)
{
if (deviceItem.GetService<SoftwareContainer>() != null)
{
var sw = deviceItem.GetService<SoftwareContainer>().Software;
Console.WriteLine($"Procesando PLC: {deviceItem.Name}");
// Exportar bloques en SIMATIC SD
foreach (var block in sw.BlockGroup.Blocks)
{
try
{
string blockExportDir = Path.Combine(exportDir, deviceItem.Name, "SIMATIC_SD");
Directory.CreateDirectory(blockExportDir);
// ¡AQUÍ ESTÁ EL MÉTODO SIMATIC SD!
block.ExportAsDocuments(
new DirectoryInfo(blockExportDir),
block.Name
);
Console.WriteLine($"✅ {block.Name} exportado en SIMATIC SD");
}
catch (Exception blockEx)
{
Console.WriteLine($"❌ Error con {block.Name}: {blockEx.Message}");
}
}
}
}
}
Console.WriteLine("🎉 Exportación SIMATIC SD completada");
}
catch (Exception ex)
{
Console.WriteLine($"❌ Error: {ex.Message}");
}
finally
{
project?.Close();
portal?.Close();
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,424 +1,242 @@
--- Log de Ejecución: x4.py ---
Grupo: ObtainIOFromProjectTia
Directorio de Trabajo: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport
Inicio: 2025-05-05 13:26:23
Fin: 2025-05-05 13:35:16
Duración: 0:08:53.119788
Directorio de Trabajo: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\SourceDoc\SourceXML
Inicio: 2025-05-20 12:12:51
Fin: 2025-05-20 12:20:01
Duración: 0:07:09.648304
Estado: SUCCESS (Código de Salida: 0)
--- SALIDA ESTÁNDAR (STDOUT) ---
--- TIA Portal Cross-Reference Exporter ---
Selected Project: C:/Trabajo/SIDEL/06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)/InLavoro/PLC/SAE196_c0.2/SAE196_c0.2.ap18
Using Base Export Directory: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport
Selected Project: C:/Trabajo/SIDEL/09 - SAE452 - Diet as Regular - San Giovanni in Bosco/Reporte/SourceDoc/Migration/SAE452/SAE452.ap18
Using Base Export Directory: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\SourceDoc\SourceXML
Connecting to TIA Portal V18.0...
2025-05-05 13:26:29,175 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Global OpenPortal - Start TIA Portal, please acknowledge the security dialog.
2025-05-05 13:26:29,200 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Global OpenPortal - With user interface
2025-05-20 12:12:56,780 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Global OpenPortal - Start TIA Portal, please acknowledge the security dialog.
2025-05-20 12:12:56,804 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Global OpenPortal - With user interface
Connected to TIA Portal.
2025-05-05 13:27:07,831 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Portal GetProcessId - Process id: 5272
Portal Process ID: 5272
Opening project: SAE196_c0.2.ap18...
2025-05-05 13:27:08,303 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Portal OpenProject - Open project... C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\InLavoro\PLC\SAE196_c0.2\SAE196_c0.2.ap18
2025-05-20 12:13:30,582 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Portal GetProcessId - Process id: 21952
Portal Process ID: 21952
Opening project: SAE452.ap18...
2025-05-20 12:13:31,077 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Portal OpenProject - Open project... C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\SourceDoc\Migration\SAE452\SAE452.ap18
Project opened successfully.
2025-05-05 13:27:39,932 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Project GetPlcs - Found plc PLC with parent name S71500/ET200MP station_1
2025-05-20 12:14:15,234 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Project GetPlcs - Found plc CPU 315F-2 PN/DP with parent name _SSAE0452
Found 1 PLC(s). Starting cross-reference export process...
--- Processing PLC: PLC ---
--- Processing PLC: CPU 315F-2 PN/DP ---
[PLC: PLC] Exporting Program Block Cross-References...
Target: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\PLC\ProgramBlocks_CR
Found 380 program blocks.
Processing block: _CYCL_EXC...
Exporting cross-references for _CYCL_EXC...
Processing block: COMPLETE RESTART...
Exporting cross-references for COMPLETE RESTART...
Processing block: I/O_FLT1...
Exporting cross-references for I/O_FLT1...
Processing block: MOD_ERR...
Exporting cross-references for MOD_ERR...
Processing block: ProDiagOB...
Exporting cross-references for ProDiagOB...
Processing block: Programming error...
Exporting cross-references for Programming error...
Processing block: RACK_FLT...
Exporting cross-references for RACK_FLT...
Processing block: Time error interrupt...
Exporting cross-references for Time error interrupt...
Processing block: Baialage...
Exporting cross-references for Baialage...
Processing block: BlenderCtrl__Main...
Exporting cross-references for BlenderCtrl__Main...
Processing block: BlenderCtrl_CIPModeInit...
Exporting cross-references for BlenderCtrl_CIPModeInit...
Processing block: BlenderCtrl_ProdModeInit...
Exporting cross-references for BlenderCtrl_ProdModeInit...
Processing block: BlenderCtrl_ResetSPWord...
Exporting cross-references for BlenderCtrl_ResetSPWord...
Processing block: BlenderCtrl_UpdatePWord...
Exporting cross-references for BlenderCtrl_UpdatePWord...
Processing block: BlenderPID_NextRecipe...
Exporting cross-references for BlenderPID_NextRecipe...
Processing block: BlenderRinse...
Exporting cross-references for BlenderRinse...
Processing block: BlenderRinse_Done...
Exporting cross-references for BlenderRinse_Done...
Processing block: BlenderRun_ProdTime...
Exporting cross-references for BlenderRun_ProdTime...
Processing block: BlenderRun_Stopping...
Exporting cross-references for BlenderRun_Stopping...
Processing block: Blocco_1...
Exporting cross-references for Blocco_1...
Processing block: Block_compare...
Exporting cross-references for Block_compare...
Processing block: Block_move...
Exporting cross-references for Block_move...
Processing block: CarboWaterLine_Seq...
Exporting cross-references for CarboWaterLine_Seq...
Processing block: Cetrifugal_Head...
Exporting cross-references for Cetrifugal_Head...
Processing block: CIP CVQ...
Exporting cross-references for CIP CVQ...
Processing block: CIP FlipFlop...
Exporting cross-references for CIP FlipFlop...
Processing block: CIPLocal_ProgInizialize...
Exporting cross-references for CIPLocal_ProgInizialize...
Processing block: CIPLocal_WaitEvent_Ctrl...
Exporting cross-references for CIPLocal_WaitEvent_Ctrl...
Processing block: CIPMain...
Exporting cross-references for CIPMain...
Processing block: CIPMain_Flood...
Exporting cross-references for CIPMain_Flood...
Processing block: CIPMain_Total Drain...
Exporting cross-references for CIPMain_Total Drain...
Processing block: Clock Signal...
Exporting cross-references for Clock Signal...
Processing block: CO2 Solubility...
Exporting cross-references for CO2 Solubility...
Processing block: CO2EqPress...
Exporting cross-references for CO2EqPress...
Processing block: CO2InjPressure...
Exporting cross-references for CO2InjPressure...
Processing block: CTRLCoolingSystem...
Exporting cross-references for CTRLCoolingSystem...
Processing block: DeairCO2TempComp...
Exporting cross-references for DeairCO2TempComp...
Processing block: DeaireationValve...
Exporting cross-references for DeaireationValve...
Processing block: Deaireator StartUp_Seq...
Exporting cross-references for Deaireator StartUp_Seq...
Processing block: DeltaP...
Exporting cross-references for DeltaP...
Processing block: FeedForward...
Exporting cross-references for FeedForward...
Processing block: Flow_To_Press_Loss...
Exporting cross-references for Flow_To_Press_Loss...
Processing block: Freq_To_mmH2O...
Exporting cross-references for Freq_To_mmH2O...
Processing block: FrictionLoss...
Exporting cross-references for FrictionLoss...
Processing block: FW_DRand...
Exporting cross-references for FW_DRand...
Processing block: GetProdBrixCO2_Anal_Inpt...
Exporting cross-references for GetProdBrixCO2_Anal_Inpt...
Processing block: Interlocking_Panel_1...
Exporting cross-references for Interlocking_Panel_1...
Processing block: ITC Communic CIPRoom...
Exporting cross-references for ITC Communic CIPRoom...
Processing block: ITC Communic Filler...
Exporting cross-references for ITC Communic Filler...
Processing block: ITC Communic MainRoutine...
Exporting cross-references for ITC Communic MainRoutine...
Processing block: ITC Communic ProdRoom...
Exporting cross-references for ITC Communic ProdRoom...
Processing block: ITC DataIn...
Exporting cross-references for ITC DataIn...
Processing block: ITC DataOut...
Exporting cross-references for ITC DataOut...
Processing block: ITC Exchange MainRoutine...
Exporting cross-references for ITC Exchange MainRoutine...
Processing block: ITC MainRoutine...
Exporting cross-references for ITC MainRoutine...
Processing block: LIMIT_I...
Exporting cross-references for LIMIT_I...
Processing block: LIMIT_R...
Exporting cross-references for LIMIT_R...
Processing block: Maselli_PA_Control...
Exporting cross-references for Maselli_PA_Control...
Processing block: Maselli_PA_Ctrl_Transfer...
Exporting cross-references for Maselli_PA_Ctrl_Transfer...
Processing block: Maselli_PA_Ctrl_Write...
Exporting cross-references for Maselli_PA_Ctrl_Write...
Processing block: MFMAnalogValues_Totalize...
Exporting cross-references for MFMAnalogValues_Totalize...
Processing block: mmH2O_TO_Freq...
Exporting cross-references for mmH2O_TO_Freq...
Processing block: ModValveFault...
Exporting cross-references for ModValveFault...
Processing block: mPDS_SYR_PA_Control...
Exporting cross-references for mPDS_SYR_PA_Control...
Processing block: ONS_R...
Exporting cross-references for ONS_R...
Processing block: Prod Tank RunOut_Seq...
Exporting cross-references for Prod Tank RunOut_Seq...
[PLC: CPU 315F-2 PN/DP] Exporting Program Block Cross-References...
Target: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\SourceDoc\SourceXML\CPU 315F-2 PN\DP\ProgramBlocks_CR
Found 410 program blocks.
Processing block: ISOonTCP_or_TCP_Protocol...
Exporting cross-references for ISOonTCP_or_TCP_Protocol...
Processing block: PIDControl...
Exporting cross-references for PIDControl...
Processing block: DETAIL_DP_DIAG...
Exporting cross-references for DETAIL_DP_DIAG...
Processing block: Net Dosing Sys Prof...
Exporting cross-references for Net Dosing Sys Prof...
Processing block: ICS Profibus Comm...
Exporting cross-references for ICS Profibus Comm...
Processing block: GNS DriveDiag...
Exporting cross-references for GNS DriveDiag...
Processing block: HMI_Blender_Parameters...
Exporting cross-references for HMI_Blender_Parameters...
Processing block: HMI Drive...
Exporting cross-references for HMI Drive...
Processing block: GNS DriveDiagMain...
Exporting cross-references for GNS DriveDiagMain...
Processing block: Integral...
Exporting cross-references for Integral...
Processing block: LowPassFilter...
Exporting cross-references for LowPassFilter...
Processing block: SlewLimit...
Exporting cross-references for SlewLimit...
Processing block: MSE Slope...
Exporting cross-references for MSE Slope...
Processing block: Statistical_Analisys...
Exporting cross-references for Statistical_Analisys...
Processing block: Blender_Variables...
Exporting cross-references for Blender_Variables...
Processing block: BrixTracking_ProdSamples...
Exporting cross-references for BrixTracking_ProdSamples...
Processing block: Procedure_Variables...
Exporting cross-references for Procedure_Variables...
Processing block: Blender_Constants...
Exporting cross-references for Blender_Constants...
Processing block: BrixTracking_SampleTime...
Exporting cross-references for BrixTracking_SampleTime...
Processing block: Delay...
Exporting cross-references for Delay...
Processing block: CO2Tracking_ProdSamples...
Exporting cross-references for CO2Tracking_ProdSamples...
Processing block: CO2Tracking_SampleTime...
Exporting cross-references for CO2Tracking_SampleTime...
Processing block: Interlocking_Variables...
Exporting cross-references for Interlocking_Variables...
Processing block: System_RunOut_Variables...
Exporting cross-references for System_RunOut_Variables...
Processing block: CIP_Program_Variables...
Exporting cross-references for CIP_Program_Variables...
Processing block: Filler_Head_Variables...
Exporting cross-references for Filler_Head_Variables...
Processing block: Filling_Time_Tranfer_DB...
Exporting cross-references for Filling_Time_Tranfer_DB...
Processing block: Blender_Variables_Pers...
Exporting cross-references for Blender_Variables_Pers...
Processing block: HMI_Alarms...
Exporting cross-references for HMI_Alarms...
Processing block: HMI_Local_CIP_Variables...
Exporting cross-references for HMI_Local_CIP_Variables...
Processing block: HMI_Service...
Exporting cross-references for HMI_Service...
Processing block: HMI_Variables_Cmd...
Exporting cross-references for HMI_Variables_Cmd...
Processing block: HMI_Variables_Status...
Exporting cross-references for HMI_Variables_Status...
Processing block: HMI_Device...
Exporting cross-references for HMI_Device...
Processing block: HMI_Instrument...
Exporting cross-references for HMI_Instrument...
Processing block: HMI_Digital...
Exporting cross-references for HMI_Digital...
Processing block: HMI_PID...
Exporting cross-references for HMI_PID...
Processing block: HMI_ICS...
Exporting cross-references for HMI_ICS...
Processing block: HMI_Device_AVS...
Exporting cross-references for HMI_Device_AVS...
Processing block: Profibus_Variables...
Exporting cross-references for Profibus_Variables...
Processing block: Input_CheckFlowMetersSta...
Exporting cross-references for Input_CheckFlowMetersSta...
Processing block: Input_DigitalScanner...
Exporting cross-references for Input_DigitalScanner...
Processing block: ProductLiterInTank...
Exporting cross-references for ProductLiterInTank...
Processing block: ProductPipeDrain_Seq...
Exporting cross-references for ProductPipeDrain_Seq...
Processing block: ProductPipeRunOut_Seq...
Exporting cross-references for ProductPipeRunOut_Seq...
Processing block: ProductQuality...
Exporting cross-references for ProductQuality...
Processing block: ProductAvailable...
Exporting cross-references for ProductAvailable...
Processing block: T_Timer...
Exporting cross-references for T_Timer...
Processing block: SEL_I...
Exporting cross-references for SEL_I...
Processing block: SEL_R...
Exporting cross-references for SEL_R...
Processing block: SelCheckBrixSource...
Exporting cross-references for SelCheckBrixSource...
Processing block: SLIM_Block...
Exporting cross-references for SLIM_Block...
Processing block: SpeedAdjust...
Exporting cross-references for SpeedAdjust...
Processing block: Syrup Line MFM Prep_Seq...
Exporting cross-references for Syrup Line MFM Prep_Seq...
Processing block: Syrup MFM StartUp_Seq...
Exporting cross-references for Syrup MFM StartUp_Seq...
Processing block: SyrupDensity...
Exporting cross-references for SyrupDensity...
Processing block: SyrupRoomCtrl...
Exporting cross-references for SyrupRoomCtrl...
Processing block: WaterDensity...
Exporting cross-references for WaterDensity...
Processing block: WritePeripheral...
Exporting cross-references for WritePeripheral...
Processing block: CIPRecipeManagement_Data...
Exporting cross-references for CIPRecipeManagement_Data...
Processing block: Co2_Counters_DB...
Exporting cross-references for Co2_Counters_DB...
Processing block: Default_SupervisionDB...
Exporting cross-references for Default_SupervisionDB...
Processing block: ITC Communic CIP DI...
Exporting cross-references for ITC Communic CIP DI...
Processing block: ITC Communic Filler DI...
Exporting cross-references for ITC Communic Filler DI...
Processing block: ITC Communic Mixer DI...
Exporting cross-references for ITC Communic Mixer DI...
Processing block: ITC Communic Product Room DI...
Exporting cross-references for ITC Communic Product Room DI...
Processing block: Key Read & Write Data...
Exporting cross-references for Key Read & Write Data...
Processing block: mPPM303StartUpRamp...
Exporting cross-references for mPPM303StartUpRamp...
Processing block: PID_RMM304_Data...
Exporting cross-references for PID_RMM304_Data...
Processing block: PID_RVN302_Data...
Exporting cross-references for PID_RVN302_Data...
Processing block: PID_RVS318_Data...
Exporting cross-references for PID_RVS318_Data...
Processing block: ProdBrixRecovery_DB...
Exporting cross-references for ProdBrixRecovery_DB...
Processing block: Prod Tank Drain_Seq...
Exporting cross-references for Prod Tank Drain_Seq...
Processing block: _StepMove...
Exporting cross-references for _StepMove...
Processing block: _StepMove_Test...
Exporting cross-references for _StepMove_Test...
Processing block: RecipeManagement_Data...
Exporting cross-references for RecipeManagement_Data...
Processing block: Blender_Procedure Data...
Exporting cross-references for Blender_Procedure Data...
Processing block: BlenderPID__Main_Data...
Exporting cross-references for BlenderPID__Main_Data...
Processing block: BlenderRun_MeasFil_Data...
Exporting cross-references for BlenderRun_MeasFil_Data...
Processing block: BrixTracking_Data...
Exporting cross-references for BrixTracking_Data...
Processing block: CO2Tracking_Data...
Exporting cross-references for CO2Tracking_Data...
Processing block: FirstProduction_Data...
Exporting cross-references for FirstProduction_Data...
Processing block: Input_Data...
Exporting cross-references for Input_Data...
Processing block: ISOonTCP_or_TCP_Protocol_DB...
Exporting cross-references for ISOonTCP_or_TCP_Protocol_DB...
Processing block: MFM_Analog_Value_Data...
Exporting cross-references for MFM_Analog_Value_Data...
Processing block: PID MAIN Data...
Exporting cross-references for PID MAIN Data...
Processing block: PID_Filling_Head_Data...
Exporting cross-references for PID_Filling_Head_Data...
Processing block: PID_RMM301_Data...
Exporting cross-references for PID_RMM301_Data...
Processing block: PID_RMM303_Data...
Exporting cross-references for PID_RMM303_Data...
Processing block: PID_RMP302_Data...
Exporting cross-references for PID_RMP302_Data...
Processing block: PID_RVM301_Data...
Exporting cross-references for PID_RVM301_Data...
Processing block: PID_RVM319_Data...
Exporting cross-references for PID_RVM319_Data...
Processing block: PID_RVP303_Data...
Exporting cross-references for PID_RVP303_Data...
Processing block: Sel_Check_Brix_Data...
Exporting cross-references for Sel_Check_Brix_Data...
Processing block: Signal_Gen_Data...
Exporting cross-references for Signal_Gen_Data...
Processing block: ProductPipeDrain_Seq...
Exporting cross-references for ProductPipeDrain_Seq...
Processing block: ProductPipeDrain...
Exporting cross-references for ProductPipeDrain...
Processing block: ProductPipeRunOut_Seq...
Exporting cross-references for ProductPipeRunOut_Seq...
Processing block: SEL_R...
Exporting cross-references for SEL_R...
Processing block: ProductPipeRunOut...
Exporting cross-references for ProductPipeRunOut...
Processing block: LIMIT_I...
Exporting cross-references for LIMIT_I...
Processing block: System_Run_Out...
Exporting cross-references for System_Run_Out...
Processing block: System_Run_Out_Data...
Exporting cross-references for System_Run_Out_Data...
Processing block: SubCarb_DB...
Exporting cross-references for SubCarb_DB...
Processing block: CYC_INT5...
Exporting cross-references for CYC_INT5...
Processing block: BlenderCtrl_All Auto...
Exporting cross-references for BlenderCtrl_All Auto...
Processing block: BlenderCtrl_InitErrors...
Exporting cross-references for BlenderCtrl_InitErrors...
Processing block: BlenderCtrl_ManualActive...
Exporting cross-references for BlenderCtrl_ManualActive...
Processing block: BlenderCtrl_MFM Command...
Exporting cross-references for BlenderCtrl_MFM Command...
Processing block: BlenderPID_FlowMeterErro...
Exporting cross-references for BlenderPID_FlowMeterErro...
Processing block: BlenderPID_PIDResInteg...
Exporting cross-references for BlenderPID_PIDResInteg...
Processing block: BlenderPIDCtrl_PresRelea...
Exporting cross-references for BlenderPIDCtrl_PresRelea...
Processing block: BlenderPIDCtrl_SaveValve...
Exporting cross-references for BlenderPIDCtrl_SaveValve...
Processing block: BlenderRun__Control...
Exporting cross-references for BlenderRun__Control...
Processing block: BlenderRun_SelectConstan...
Exporting cross-references for BlenderRun_SelectConstan...
Processing block: BlendFill StartUp_Seq...
Exporting cross-references for BlendFill StartUp_Seq...
Processing block: CIP_SimpleProgr_Init...
Exporting cross-references for CIP_SimpleProgr_Init...
Processing block: CIPLocal...
Exporting cross-references for CIPLocal...
Processing block: CIPLocal_ExecSimpleCIP...
Exporting cross-references for CIPLocal_ExecSimpleCIP...
Processing block: CIPLocal_ExecStep...
Exporting cross-references for CIPLocal_ExecStep...
Processing block: CIPLocal_ProgStepDown...
Exporting cross-references for CIPLocal_ProgStepDown...
Processing block: CIPLocal_ProgStepUp...
Exporting cross-references for CIPLocal_ProgStepUp...
Processing block: CIPReportManager...
Exporting cross-references for CIPReportManager...
ERROR accessing Program Blocks for cross-reference export: RemotingException: El objeto '/460a527c_f027_40c0_bbfb_2f9184c04002/hwhq0szmkxqfz2pc1xmghz0a_310.rem' se desconectó o no existe en el servidor.
ERROR accessing Program Blocks for cross-reference export: RemotingException: El objeto '/bd68de4c_3307_463d_b3ce_57a1378b3bde/lnycb9uriadrpgmom_mjdspm_249.rem' se desconectó o no existe en el servidor.
[PLC: PLC] Exporting PLC Tag Table Cross-References...
Target: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\PLC\PlcTags_CR
Found 10 Tag Tables.
Processing Tag Table: Memories...
Exporting cross-references for Memories...
Processing Tag Table: Tabella delle variabili standard...
Exporting cross-references for Tabella delle variabili standard...
Processing Tag Table: Timers_Counters...
Exporting cross-references for Timers_Counters...
Processing Tag Table: Inputs...
Exporting cross-references for Inputs...
Processing Tag Table: Outputs...
Exporting cross-references for Outputs...
Processing Tag Table: Tabella delle variabili_1...
Exporting cross-references for Tabella delle variabili_1...
Processing Tag Table: Tabella delle variabili_2...
Exporting cross-references for Tabella delle variabili_2...
Processing Tag Table: OutputsFesto...
Exporting cross-references for OutputsFesto...
Processing Tag Table: InputsMaster...
Exporting cross-references for InputsMaster...
Processing Tag Table: OutputsMaster...
Exporting cross-references for OutputsMaster...
Tag Table CR Export Summary: Exported=10, Skipped/Errors=0
[PLC: CPU 315F-2 PN/DP] Exporting PLC Tag Table Cross-References...
Target: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\SourceDoc\SourceXML\CPU 315F-2 PN\DP\PlcTags_CR
Found 2 Tag Tables.
Processing Tag Table: Default tag table...
Exporting cross-references for Default tag table...
Processing Tag Table: STEP7 classic symbols...
Exporting cross-references for STEP7 classic symbols...
Tag Table CR Export Summary: Exported=2, Skipped/Errors=0
[PLC: PLC] Exporting PLC Data Type (UDT) Cross-References...
Target: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\PLC\PlcDataTypes_CR
Found 24 UDTs.
Processing UDT: AnalogInstrument...
Exporting cross-references for AnalogInstrument...
Processing UDT: CIP_Link_Type...
Exporting cross-references for CIP_Link_Type...
Processing UDT: CIP_Simple_Type...
Exporting cross-references for CIP_Simple_Type...
Processing UDT: CIP_Step_Type...
Exporting cross-references for CIP_Step_Type...
Processing UDT: CIP_WaitEvent_Type...
Exporting cross-references for CIP_WaitEvent_Type...
Processing UDT: Device...
Exporting cross-references for Device...
Processing UDT: DigitalInstrument...
Exporting cross-references for DigitalInstrument...
[PLC: CPU 315F-2 PN/DP] Exporting PLC Data Type (UDT) Cross-References...
Target: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\SourceDoc\SourceXML\CPU 315F-2 PN\DP\PlcDataTypes_CR
Found 21 UDTs.
Processing UDT: AnyPoint...
Exporting cross-references for AnyPoint...
Processing UDT: FunctionButton...
Exporting cross-references for FunctionButton...
Processing UDT: PID...
Exporting cross-references for PID...
Processing UDT: QCO Phase...
Exporting cross-references for QCO Phase...
Processing UDT: QCO Spare...
Exporting cross-references for QCO Spare...
Processing UDT: QCO Timer...
Exporting cross-references for QCO Timer...
Processing UDT: QCO Timer_Array_1...
Exporting cross-references for QCO Timer_Array_1...
Processing UDT: Recipe_Prod...
Exporting cross-references for Recipe_Prod...
Processing UDT: ReportCIPSimpleData...
Exporting cross-references for ReportCIPSimpleData...
Processing UDT: TADDR_PAR...
Exporting cross-references for TADDR_PAR...
Processing UDT: Device...
Exporting cross-references for Device...
Processing UDT: AnalogInstrument...
Exporting cross-references for AnalogInstrument...
Processing UDT: DigitalInstrument...
Exporting cross-references for DigitalInstrument...
Processing UDT: PID...
Exporting cross-references for PID...
Processing UDT: EHS16...
Exporting cross-references for EHS16...
Processing UDT: Danfoss Diag...
Exporting cross-references for Danfoss Diag...
Processing UDT: QCO Timer...
Exporting cross-references for QCO Timer...
Processing UDT: QCO Phase...
Exporting cross-references for QCO Phase...
Processing UDT: ReportCIPSimpleData...
Exporting cross-references for ReportCIPSimpleData...
Processing UDT: CIP_WaitEvent_Type...
Exporting cross-references for CIP_WaitEvent_Type...
Processing UDT: CIP_Step_Type_New...
Exporting cross-references for CIP_Step_Type_New...
Processing UDT: CIP_Simple_Type...
Exporting cross-references for CIP_Simple_Type...
Processing UDT: CIP_Link_Type...
Exporting cross-references for CIP_Link_Type...
Processing UDT: CIP_Step_Type...
Exporting cross-references for CIP_Step_Type...
Processing UDT: Recipe_Prod...
Exporting cross-references for Recipe_Prod...
Processing UDT: ICS Hndsk receive signal...
Exporting cross-references for ICS Hndsk receive signal...
Processing UDT: ICS Hndsk send signal...
Exporting cross-references for ICS Hndsk send signal...
Processing UDT: TCON_PAR...
Exporting cross-references for TCON_PAR...
Processing UDT: TCON_PAR_LF...
Exporting cross-references for TCON_PAR_LF...
Processing UDT: Tipo di dati utente_1...
Exporting cross-references for Tipo di dati utente_1...
Processing UDT: Tipo di dati utente_2...
Exporting cross-references for Tipo di dati utente_2...
Processing UDT: ASLeds...
Exporting cross-references for ASLeds...
Processing UDT: IFLeds...
Exporting cross-references for IFLeds...
Processing UDT: SV_FB_State...
Exporting cross-references for SV_FB_State...
Processing UDT: SV_State...
Exporting cross-references for SV_State...
UDT CR Export Summary: Exported=24, Skipped/Errors=0
UDT CR Export Summary: Exported=21, Skipped/Errors=0
[PLC: PLC] Attempting to Export System Block Cross-References...
Target: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\PLC\SystemBlocks_CR
Found 9 system blocks (using get_system_blocks).
Processing System Block: T_301...
Exporting cross-references for T_301...
Processing System Block: IEC_Timer_0_DB_9...
Exporting cross-references for IEC_Timer_0_DB_9...
Processing System Block: T_302...
Exporting cross-references for T_302...
Processing System Block: GET_Reciver...
Exporting cross-references for GET_Reciver...
Processing System Block: PUT_Send_Filler...
Exporting cross-references for PUT_Send_Filler...
Processing System Block: LED...
Exporting cross-references for LED...
Processing System Block: SCALE...
Exporting cross-references for SCALE...
Processing System Block: CONT_C...
Exporting cross-references for CONT_C...
Processing System Block: DeviceStates...
Exporting cross-references for DeviceStates...
System Block CR Export Summary: Exported=9, Skipped/Errors=0
[PLC: CPU 315F-2 PN/DP] Attempting to Export System Block Cross-References...
Target: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\SourceDoc\SourceXML\CPU 315F-2 PN\DP\SystemBlocks_CR
Found 12 system blocks (using get_system_blocks).
Processing System Block: TIM_S5TI...
Exporting cross-references for TIM_S5TI...
Processing System Block: REPLACE...
Exporting cross-references for REPLACE...
Processing System Block: LIMIT...
Exporting cross-references for LIMIT...
Processing System Block: NE_STRNG...
Exporting cross-references for NE_STRNG...
Processing System Block: TURCV...
Exporting cross-references for TURCV...
Processing System Block: TUSEND...
Exporting cross-references for TUSEND...
Processing System Block: PID_Continuos...
Exporting cross-references for PID_Continuos...
Processing System Block: TDISCON...
Exporting cross-references for TDISCON...
Processing System Block: TCON...
Exporting cross-references for TCON...
Processing System Block: TRCV...
Exporting cross-references for TRCV...
Processing System Block: TSEND...
Exporting cross-references for TSEND...
Processing System Block: DT_DATE...
Exporting cross-references for DT_DATE...
System Block CR Export Summary: Exported=12, Skipped/Errors=0
[PLC: PLC] Attempting to Export Software Unit Cross-References...
Target: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\PLC\SoftwareUnits_CR
[PLC: CPU 315F-2 PN/DP] Attempting to Export Software Unit Cross-References...
Target: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\SourceDoc\SourceXML\CPU 315F-2 PN\DP\SoftwareUnits_CR
Found 0 Software Units.
Software Unit CR Export Summary: Exported=0, Skipped/Errors=0
--- Finished processing PLC: PLC ---
--- Finished processing PLC: CPU 315F-2 PN/DP ---
Cross-reference export process completed.
Closing TIA Portal...
2025-05-05 13:35:02,332 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Portal ClosePortal - Close TIA Portal
2025-05-20 12:19:54,187 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Portal ClosePortal - Close TIA Portal
TIA Portal closed.
Script finished.
@ -428,6 +246,6 @@ Traceback (most recent call last):
File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 99, in export_plc_cross_references
block_name = block.get_name()
^^^^^^^^^^^^^^^^
ValueError: RemotingException: El objeto '/460a527c_f027_40c0_bbfb_2f9184c04002/hwhq0szmkxqfz2pc1xmghz0a_310.rem' se desconectó o no existe en el servidor.
ValueError: RemotingException: El objeto '/bd68de4c_3307_463d_b3ce_57a1378b3bde/lnycb9uriadrpgmom_mjdspm_249.rem' se desconectó o no existe en el servidor.
--- FIN DEL LOG ---

View File

@ -0,0 +1,81 @@
--- Log de Ejecución: xTest.py ---
Grupo: ObtainIOFromProjectTia
Directorio de Trabajo: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\SourceDoc\SourcdSD
Inicio: 2025-05-22 11:17:27
Fin: 2025-05-22 11:18:44
Duración: 0:01:16.758340
Estado: ERROR (Código de Salida: 1)
--- SALIDA ESTÁNDAR (STDOUT) ---
============================================================
PRUEBA DE EXPORTACIÓN SIMATIC SD - TIA PORTAL V20
============================================================
Project: C:/Trabajo/SIDEL/09 - SAE452 - Diet as Regular - San Giovanni in Bosco/Reporte/SourceDoc/Migration/SAE452_V20/SAE452_V20.ap20
Export Directory: C:/Users/migue/Downloads/Nueva carpeta (18)\SIMATIC_SD_Test
Connecting to TIA Portal V20...
2025-05-22 11:17:49,266 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Global OpenPortal - Start TIA Portal, please acknowledge the security dialog.
2025-05-22 11:17:49,283 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Global OpenPortal - With user interface
Connected successfully.
Opening project...
2025-05-22 11:18:05,562 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Portal OpenProject - Open project... C:/Trabajo/SIDEL/09 - SAE452 - Diet as Regular - San Giovanni in Bosco/Reporte/SourceDoc/Migration/SAE452_V20/SAE452_V20.ap20
Project opened successfully.
2025-05-22 11:18:20,088 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Project GetPlcs - Found plc CPU 315F-2 PN/DP with parent name _SSAE0452
Found 1 PLC(s)
Testing with PLC: CPU 315F-2 PN/DP
Found 410 program blocks
--- Testing Block 1/3: ISOonTCP_or_TCP_Protocol ---
Programming Language: STL
Available methods on block:
- export
- export_cross_references
✗ ExportAsDocuments method NOT found
Available methods containing 'export':
- export
- export_cross_references
--- Testing Block 2/3: PIDControl ---
Compiling block...
2025-05-22 11:18:24,970 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.ProgramBlock Compile - Compile the PLC program block PIDControl. Result:
2025-05-22 11:18:31,184 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.ProgramBlock Compile - Warning: CPU 315F-2 PN/DP > General warnings > Inputs or outputs are used that do not exist in the configured hardware.
2025-05-22 11:18:31,185 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.ProgramBlock Compile - Warning: CPU 315F-2 PN/DP > Compiling finished (errors: 0; warnings: 1)
Programming Language: LAD
Available methods on block:
- export
- export_cross_references
✗ ExportAsDocuments method NOT found
Available methods containing 'export':
- export
- export_cross_references
--- Testing Block 3/3: DETAIL_DP_DIAG ---
Programming Language: STL
Available methods on block:
- export
- export_cross_references
✗ ExportAsDocuments method NOT found
Available methods containing 'export':
- export
- export_cross_references
============================================================
PRUEBA COMPLETADA
============================================================
No se crearon archivos en C:/Users/migue/Downloads/Nueva carpeta (18)\SIMATIC_SD_Test
Closing TIA Portal...
2025-05-22 11:18:31,209 [1] INFO Siemens.TiaPortal.OpennessApi19.Implementations.Portal ClosePortal - Close TIA Portal
Press Enter to exit...
--- ERRORES (STDERR) ---
Traceback (most recent call last):
File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\xTest.py", line 215, in <module>
input("\nPress Enter to exit...")
EOFError: EOF when reading a line
--- FIN DEL LOG ---

View File

@ -5,5 +5,5 @@
},
"level2": {},
"level3": {},
"working_directory": "C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\SourceDoc\\SourceXML"
"working_directory": "C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\SourceDoc\\SourcdSD"
}

View File

@ -1,6 +1,6 @@
{
"x1.py": {
"display_name": "1: Exportar Lógica desde TIA Portal",
"display_name": "1: Exportar Lógica desde TIA Portal v18 en XML",
"short_description": "Exporta la lógica del PLC desde TIA Portal en archivos XML y SCL.",
"long_description": "Este script utiliza TIA Portal Openness para exportar la lógica de un PLC en formato XML y SCL. Permite seleccionar un proyecto de TIA Portal y genera los archivos de exportación en el directorio configurado.\n***\n**Lógica Principal:**\n\n1. **Configuración:** Carga parámetros desde `ParamManagerScripts` (directorio de trabajo, versión de TIA Portal).\n2. **Selección de Proyecto:** Abre un cuadro de diálogo para seleccionar el archivo del proyecto de TIA Portal.\n3. **Conexión a TIA Portal:** Utiliza la API de TIA Openness para conectarse al portal y abrir el proyecto seleccionado.\n4. **Exportación:** Exporta la lógica del PLC en archivos XML y SCL al directorio configurado.\n5. **Cierre:** Cierra la conexión con TIA Portal al finalizar.",
"hidden": false
@ -10,5 +10,17 @@
"short_description": "Script para exportar las referencias cruzadas",
"long_description": "",
"hidden": false
},
"x2.py": {
"display_name": "2: Exportar Lógica desde TIA Portal V20 en SIMATIC SD format",
"short_description": "export_logic_from_tia_v20_simatic_sd : Script para exportar el software de un PLC desde TIA Portal V20",
"long_description": "",
"hidden": false
},
"xTest.py": {
"display_name": "xTest",
"short_description": "Test específico para exportación SIMATIC SD usando ExportAsDocuments()",
"long_description": "",
"hidden": false
}
}

View File

@ -1,7 +1,7 @@
{
"path": "C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\SourceDoc\\SourceXML",
"path": "C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\SourceDoc\\SourcdSD",
"history": [
"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"
"C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\SourceDoc\\SourcdSD",
"C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\SourceDoc\\SourceXML"
]
}

View File

@ -0,0 +1,462 @@
"""
export_simatic_sd_tia20 : Script para exportar bloques de PLC desde TIA Portal 20 en formato SIMATIC SD.
Basado en la nueva funcionalidad de exportación SIMATIC SD disponible en TIA Portal V20.
"""
import tkinter as tk
from tkinter import filedialog
import os
import sys
import traceback
script_root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
)
sys.path.append(script_root)
from backend.script_utils import load_configuration
# --- Configuration ---
TIA_PORTAL_VERSION = "20.0" # Target TIA Portal version for SIMATIC SD support
EXPORT_OPTIONS = None # Use default export options
KEEP_FOLDER_STRUCTURE = (
True # Replicate TIA project folder structure in export directory
)
# --- TIA Scripting Import Handling ---
# Check if the TIA_SCRIPTING environment variable is set
if os.getenv("TIA_SCRIPTING"):
sys.path.append(os.getenv("TIA_SCRIPTING"))
else:
# Optional: Define a fallback path if the environment variable isn't set
# fallback_path = "C:\\path\\to\\your\\TIA_Scripting_binaries"
# if os.path.exists(fallback_path):
# sys.path.append(fallback_path)
pass # Allow import to fail if not found
try:
import siemens_tia_scripting as ts
EXPORT_OPTIONS = (
ts.Enums.ExportOptions.WithDefaults
) # Set default options now that 'ts' is imported
except ImportError:
print("ERROR: Failed to import 'siemens_tia_scripting'.")
print("Ensure:")
print(f"1. TIA Portal Openness for V{TIA_PORTAL_VERSION} is installed.")
print(
"2. The 'siemens_tia_scripting' Python module is installed (pip install ...) or"
)
print(
" the path to its binaries is set in the 'TIA_SCRIPTING' environment variable."
)
print(
"3. You are using a compatible Python version (e.g., 3.12.X as per documentation)."
)
sys.exit(1)
except Exception as e:
print(f"An unexpected error occurred during import: {e}")
traceback.print_exc()
sys.exit(1)
# --- Functions ---
def select_project_file():
"""Opens a dialog to select a TIA Portal project file."""
root = tk.Tk()
root.withdraw() # Hide the main tkinter window
file_path = filedialog.askopenfilename(
title="Select TIA Portal Project File",
filetypes=[
(
f"TIA Portal V{TIA_PORTAL_VERSION} Projects",
f"*.ap{TIA_PORTAL_VERSION.split('.')[0]}",
)
], # e.g. *.ap20
)
root.destroy()
if not file_path:
print("No project file selected. Exiting.")
sys.exit(0)
return file_path
def select_export_directory():
"""Opens a dialog to select the export directory."""
root = tk.Tk()
root.withdraw() # Hide the main tkinter window
dir_path = filedialog.askdirectory(title="Select Export Directory")
root.destroy()
if not dir_path:
print("No export directory selected. Exiting.")
sys.exit(0)
return dir_path
def check_simatic_sd_support():
"""Verifies if SIMATIC SD format is available in the current TIA Scripting version."""
try:
# Check if SimaticSD is available in ExportFormats enum
simatic_sd_format = ts.Enums.ExportFormats.SimaticSD
print(f"✓ SIMATIC SD format supported (enum value: {simatic_sd_format})")
return True
except AttributeError:
print("✗ ERROR: SIMATIC SD format not available in this TIA Scripting version.")
print("Please ensure you are using TIA Portal V20 or later with compatible TIA Scripting.")
return False
def export_plc_data_simatic_sd(plc, export_base_dir):
"""Exports Blocks, UDTs, and Tag Tables from a given PLC in SIMATIC SD format."""
plc_name = plc.get_name()
print(f"\n--- Processing PLC: {plc_name} (SIMATIC SD Export) ---")
# Define base export path for this PLC with timestamp to avoid conflicts
import datetime
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
plc_export_dir = os.path.join(export_base_dir, f"{plc_name}_SimaticSD_{timestamp}")
os.makedirs(plc_export_dir, exist_ok=True)
# --- Export Program Blocks in SIMATIC SD Format ---
blocks_exported = 0
blocks_skipped = 0
print(f"\n[PLC: {plc_name}] Exporting Program Blocks (SIMATIC SD)...")
sd_blocks_path = os.path.join(plc_export_dir, "01_ProgramBlocks_SD")
os.makedirs(sd_blocks_path, exist_ok=True)
print(f" SIMATIC SD Target: {sd_blocks_path}")
# Also create a separate SimaticML export for comparison
xml_blocks_path = os.path.join(plc_export_dir, "02_ProgramBlocks_XML_Compare")
os.makedirs(xml_blocks_path, exist_ok=True)
try:
program_blocks = plc.get_program_blocks()
print(f" Found {len(program_blocks)} program blocks.")
for block in program_blocks:
block_name = block.get_name()
print(f" Processing block: {block_name}...")
try:
if not block.is_consistent():
print(f" Compiling block {block_name}...")
block.compile()
if not block.is_consistent():
print(
f" WARNING: Block {block_name} inconsistent after compile. Skipping."
)
blocks_skipped += 1
continue
print(f" Exporting {block_name} as SIMATIC SD...")
try:
block.export(
target_directory_path=sd_blocks_path,
export_options=EXPORT_OPTIONS,
export_format=ts.Enums.ExportFormats.SimaticSD, # New SIMATIC SD format
keep_folder_structure=KEEP_FOLDER_STRUCTURE,
)
blocks_exported += 1
print(f" ✓ Successfully exported {block_name} in SIMATIC SD")
# Also export same block in XML for comparison
print(f" Exporting {block_name} as XML for comparison...")
block.export(
target_directory_path=xml_blocks_path,
export_options=EXPORT_OPTIONS,
export_format=ts.Enums.ExportFormats.SimaticML, # Traditional XML format
keep_folder_structure=KEEP_FOLDER_STRUCTURE,
)
print(f" + Also exported {block_name} in XML for comparison")
except Exception as export_ex:
print(f" ERROR during export: {export_ex}")
# Try to export only in XML if SD fails
try:
print(f" Attempting fallback XML export for {block_name}...")
block.export(
target_directory_path=xml_blocks_path,
export_options=EXPORT_OPTIONS,
export_format=ts.Enums.ExportFormats.SimaticML,
keep_folder_structure=KEEP_FOLDER_STRUCTURE,
)
print(f" ✓ Fallback XML export successful for {block_name}")
blocks_exported += 1
except Exception as fallback_ex:
print(f" ERROR: Both SD and XML export failed: {fallback_ex}")
blocks_skipped += 1
except Exception as block_ex:
print(f" ERROR exporting block {block_name}: {block_ex}")
blocks_skipped += 1
print(
f" Program Blocks Export Summary: Exported={blocks_exported}, Skipped/Errors={blocks_skipped}"
)
except Exception as e:
print(f" ERROR processing Program Blocks: {e}")
traceback.print_exc()
# --- Export PLC Data Types (UDTs) in SIMATIC SD Format ---
udts_exported = 0
udts_skipped = 0
print(f"\n[PLC: {plc_name}] Exporting PLC Data Types - UDTs (SIMATIC SD)...")
udt_sd_export_path = os.path.join(plc_export_dir, "03_PlcDataTypes_SD")
udt_xml_export_path = os.path.join(plc_export_dir, "04_PlcDataTypes_XML_Compare")
os.makedirs(udt_sd_export_path, exist_ok=True)
os.makedirs(udt_xml_export_path, exist_ok=True)
print(f" SIMATIC SD Target: {udt_sd_export_path}")
print(f" XML Compare Target: {udt_xml_export_path}")
try:
udts = plc.get_user_data_types()
print(f" Found {len(udts)} UDTs.")
for udt in udts:
udt_name = udt.get_name()
print(f" Processing UDT: {udt_name}...")
try:
if not udt.is_consistent():
print(f" Compiling UDT {udt_name}...")
udt.compile()
if not udt.is_consistent():
print(
f" WARNING: UDT {udt_name} inconsistent after compile. Skipping."
)
udts_skipped += 1
continue
print(f" Exporting {udt_name} as SIMATIC SD...")
try:
udt.export(
target_directory_path=udt_sd_export_path,
export_options=EXPORT_OPTIONS,
export_format=ts.Enums.ExportFormats.SimaticSD, # SIMATIC SD format for UDTs
keep_folder_structure=KEEP_FOLDER_STRUCTURE,
)
# Also export in XML for comparison
udt.export(
target_directory_path=udt_xml_export_path,
export_options=EXPORT_OPTIONS,
export_format=ts.Enums.ExportFormats.SimaticML,
keep_folder_structure=KEEP_FOLDER_STRUCTURE,
)
udts_exported += 1
print(f" ✓ Successfully exported {udt_name} (SD + XML)")
except Exception as udt_export_ex:
print(f" ERROR during UDT export: {udt_export_ex}")
udts_skipped += 1
except Exception as udt_ex:
print(f" ERROR exporting UDT {udt_name}: {udt_ex}")
udts_skipped += 1
print(
f" UDT Export Summary: Exported={udts_exported}, Skipped/Errors={udts_skipped}"
)
except Exception as e:
print(f" ERROR processing UDTs: {e}")
traceback.print_exc()
# --- Export PLC Tag Tables in SIMATIC SD Format ---
tags_exported = 0
tags_skipped = 0
print(f"\n[PLC: {plc_name}] Exporting PLC Tag Tables (SIMATIC SD)...")
tags_sd_export_path = os.path.join(plc_export_dir, "05_PlcTags_SD")
tags_xml_export_path = os.path.join(plc_export_dir, "06_PlcTags_XML_Compare")
os.makedirs(tags_sd_export_path, exist_ok=True)
os.makedirs(tags_xml_export_path, exist_ok=True)
print(f" SIMATIC SD Target: {tags_sd_export_path}")
print(f" XML Compare Target: {tags_xml_export_path}")
try:
tag_tables = plc.get_plc_tag_tables()
print(f" Found {len(tag_tables)} Tag Tables.")
for table in tag_tables:
table_name = table.get_name()
print(f" Processing Tag Table: {table_name}...")
try:
print(f" Exporting {table_name} as SIMATIC SD...")
try:
table.export(
target_directory_path=tags_sd_export_path,
export_options=EXPORT_OPTIONS,
export_format=ts.Enums.ExportFormats.SimaticSD, # SIMATIC SD format for Tag Tables
keep_folder_structure=KEEP_FOLDER_STRUCTURE,
)
# Also export in XML for comparison
table.export(
target_directory_path=tags_xml_export_path,
export_options=EXPORT_OPTIONS,
export_format=ts.Enums.ExportFormats.SimaticML,
keep_folder_structure=KEEP_FOLDER_STRUCTURE,
)
tags_exported += 1
print(f" ✓ Successfully exported {table_name} (SD + XML)")
except Exception as tag_export_ex:
print(f" ERROR during Tag Table export: {tag_export_ex}")
tags_skipped += 1
except Exception as table_ex:
print(f" ERROR exporting Tag Table {table_name}: {table_ex}")
tags_skipped += 1
print(
f" Tag Table Export Summary: Exported={tags_exported}, Skipped/Errors={tags_skipped}"
)
except Exception as e:
print(f" ERROR processing Tag Tables: {e}")
traceback.print_exc()
print(f"\n--- Finished processing PLC: {plc_name} ---")
def export_additional_formats(plc, export_base_dir):
"""Optional: Export in traditional formats alongside SIMATIC SD for comparison."""
plc_name = plc.get_name()
print(f"\n[Optional] Exporting traditional formats for comparison - PLC: {plc_name}")
# Create comparison directory
comparison_dir = os.path.join(export_base_dir, plc_name, "Comparison_Formats")
os.makedirs(comparison_dir, exist_ok=True)
# Export a few blocks in SimaticML for comparison
xml_comparison_path = os.path.join(comparison_dir, "SimaticML_Sample")
os.makedirs(xml_comparison_path, exist_ok=True)
try:
program_blocks = plc.get_program_blocks()
# Export first 3 blocks in SimaticML for comparison
for i, block in enumerate(program_blocks[:3]):
if block.is_consistent():
block_name = block.get_name()
print(f" Exporting {block_name} in SimaticML for comparison...")
block.export(
target_directory_path=xml_comparison_path,
export_options=EXPORT_OPTIONS,
export_format=ts.Enums.ExportFormats.SimaticML,
keep_folder_structure=KEEP_FOLDER_STRUCTURE,
)
except Exception as e:
print(f" Warning: Could not export comparison formats: {e}")
# --- Main Script ---
if __name__ == "__main__":
configs = load_configuration()
working_directory = configs.get("working_directory")
print("--- TIA Portal 20 SIMATIC SD Exporter ---")
print("Exporting Blocks, UDTs, and Tags in SIMATIC SD Format")
# Check SIMATIC SD support first
if not check_simatic_sd_support():
sys.exit(1)
# Validate working directory
if not working_directory or not os.path.isdir(working_directory):
print("ERROR: Working directory not set or invalid in configuration.")
print("Please configure the working directory using the main application.")
sys.exit(1)
# 1. Select Project File, Export Directory comes from config
project_file = select_project_file()
export_dir = working_directory # Use working directory from config
print(f"\nSelected Project: {project_file}")
print(f"Using Export Directory (Working Directory): {export_dir}")
portal_instance = None
project_object = None
try:
# 2. Connect to TIA Portal V20
print(f"\nConnecting to TIA Portal V{TIA_PORTAL_VERSION}...")
portal_instance = ts.open_portal(
version=TIA_PORTAL_VERSION,
portal_mode=ts.Enums.PortalMode.WithGraphicalUserInterface,
)
print("Connected to TIA Portal V20.")
print(f"Portal Process ID: {portal_instance.get_process_id()}")
# 3. Open Project
print(f"Opening project: {os.path.basename(project_file)}...")
project_object = portal_instance.open_project(project_file_path=project_file)
if project_object is None:
print("Project might already be open, attempting to get handle...")
project_object = portal_instance.get_project()
if project_object is None:
raise Exception("Failed to open or get the specified project.")
print("Project opened successfully.")
# 4. Get PLCs
plcs = project_object.get_plcs()
if not plcs:
print("No PLC devices found in the project.")
else:
print(f"Found {len(plcs)} PLC(s). Starting SIMATIC SD export process...")
# 5. Iterate and Export Data for each PLC in SIMATIC SD format
import datetime # Add this import for timestamp
for plc_device in plcs:
export_plc_data_simatic_sd(
plc=plc_device, export_base_dir=export_dir
)
print("\n🎉 SIMATIC SD Export process completed successfully!")
print("\nExported files structure:")
print("├── [PLC_Name]_SimaticSD_[timestamp]/")
print("│ ├── 01_ProgramBlocks_SD/ # SIMATIC SD format")
print("│ ├── 02_ProgramBlocks_XML_Compare/ # Traditional XML for comparison")
print("│ ├── 03_PlcDataTypes_SD/")
print("│ ├── 04_PlcDataTypes_XML_Compare/")
print("│ ├── 05_PlcTags_SD/")
print("│ └── 06_PlcTags_XML_Compare/")
print("\nNow you can compare the differences between SIMATIC SD and traditional XML formats!")
# Add file analysis
print("\n=== FILE ANALYSIS ===")
for plc_device in plcs:
plc_name = plc_device.get_name()
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
plc_export_dir = os.path.join(export_dir, f"{plc_name}_SimaticSD_{timestamp}")
print(f"\nAnalyzing exported files for PLC: {plc_name}")
folders_to_check = [
("01_ProgramBlocks_SD", "SIMATIC SD Blocks"),
("02_ProgramBlocks_XML_Compare", "XML Blocks"),
("03_PlcDataTypes_SD", "SIMATIC SD UDTs"),
("04_PlcDataTypes_XML_Compare", "XML UDTs"),
("05_PlcTags_SD", "SIMATIC SD Tags"),
("06_PlcTags_XML_Compare", "XML Tags")
]
for folder_name, description in folders_to_check:
folder_path = os.path.join(plc_export_dir, folder_name)
if os.path.exists(folder_path):
files = [f for f in os.listdir(folder_path) if os.path.isfile(os.path.join(folder_path, f))]
print(f" {description}: {len(files)} files")
if files:
extensions = set(os.path.splitext(f)[1].lower() for f in files)
print(f" File extensions: {', '.join(extensions) if extensions else 'No extensions'}")
else:
print(f" {description}: Folder not found")
except ts.TiaException as tia_ex:
print(f"\nTIA Portal Openness Error: {tia_ex}")
traceback.print_exc()
except FileNotFoundError:
print(f"\nERROR: Project file not found at {project_file}")
except Exception as e:
print(f"\nAn unexpected error occurred: {e}")
traceback.print_exc()
finally:
# 6. Cleanup
if portal_instance:
try:
print("\nClosing TIA Portal...")
portal_instance.close_portal()
print("TIA Portal closed.")
except Exception as close_ex:
print(f"Error during TIA Portal cleanup: {close_ex}")
print("\nScript finished.")

View File

@ -1,9 +1,9 @@
--- Log de Ejecución: x3.py ---
Grupo: S7_DB_Utils
Directorio de Trabajo: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001
Inicio: 2025-05-18 02:09:01
Fin: 2025-05-18 02:09:01
Duración: 0:00:00.154928
Inicio: 2025-05-20 16:06:51
Fin: 2025-05-20 16:06:52
Duración: 0:00:00.220075
Estado: SUCCESS (Código de Salida: 0)
--- SALIDA ESTÁNDAR (STDOUT) ---
@ -11,13 +11,13 @@ Using working directory: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Gi
Los archivos JSON de salida se guardarán en: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json
Archivos encontrados para procesar: 2
--- Procesando archivo: db1001_data.db ---
Parseo completo. Intentando serializar a JSON: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json\db1001_data.json
Resultado guardado en: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json\db1001_data.json
--- Procesando archivo: DB1001_data.AWL ---
Parseo completo. Intentando serializar a JSON: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json\DB1001_data.json
Resultado guardado en: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json\DB1001_data.json
--- Procesando archivo: db1001_format.db ---
Parseo completo. Intentando serializar a JSON: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json\db1001_format.json
Resultado guardado en: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json\db1001_format.json
--- Procesando archivo: DB1001_format.AWL ---
Parseo completo. Intentando serializar a JSON: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json\DB1001_format.json
Resultado guardado en: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json\DB1001_format.json
--- Proceso completado ---

View File

@ -1,9 +1,9 @@
--- Log de Ejecución: x4.py ---
Grupo: S7_DB_Utils
Directorio de Trabajo: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001
Inicio: 2025-05-18 13:15:28
Fin: 2025-05-18 13:15:28
Duración: 0:00:00.188819
Inicio: 2025-05-20 15:49:22
Fin: 2025-05-20 15:49:23
Duración: 0:00:00.797004
Estado: SUCCESS (Código de Salida: 0)
--- SALIDA ESTÁNDAR (STDOUT) ---
@ -11,20 +11,20 @@ Using working directory: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Gi
Los archivos de documentación generados se guardarán en: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation
Archivos JSON encontrados para procesar: 3
--- Procesando archivo JSON: db1001_data.json ---
Archivo JSON 'db1001_data.json' cargado correctamente.
Archivo S7 reconstruido generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_data.txt
Archivo Markdown de documentación generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_data.md
--- Procesando archivo JSON: DB1001_data.json ---
Archivo JSON 'DB1001_data.json' cargado correctamente.
Archivo S7 reconstruido generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\DB1001_data.txt
Archivo Markdown de documentación generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\DB1001_data.md
--- Procesando archivo JSON: db1001_format.json ---
Archivo JSON 'db1001_format.json' cargado correctamente.
Archivo S7 reconstruido generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_format.txt
Archivo Markdown de documentación generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_format.md
--- Procesando archivo JSON: DB1001_format.json ---
Archivo JSON 'DB1001_format.json' cargado correctamente.
Archivo S7 reconstruido generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\DB1001_format.txt
Archivo Markdown de documentación generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\DB1001_format.md
--- Procesando archivo JSON: db1001_updated.json ---
Archivo JSON 'db1001_updated.json' cargado correctamente.
Archivo S7 reconstruido generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_updated.txt
Archivo Markdown de documentación generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_updated.md
--- Procesando archivo JSON: DB1001_updated.json ---
Archivo JSON 'DB1001_updated.json' cargado correctamente.
Archivo S7 reconstruido generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\DB1001_updated.txt
Archivo Markdown de documentación generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\DB1001_updated.md
--- Proceso de generación de documentación completado ---

View File

@ -1,9 +1,9 @@
--- Log de Ejecución: x7_value_updater.py ---
Grupo: S7_DB_Utils
Directorio de Trabajo: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001
Inicio: 2025-05-18 15:48:00
Fin: 2025-05-18 15:48:01
Duración: 0:00:00.811974
Inicio: 2025-05-23 12:38:21
Fin: 2025-05-23 12:38:23
Duración: 0:00:01.539943
Estado: SUCCESS (Código de Salida: 0)
--- SALIDA ESTÁNDAR (STDOUT) ---

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -135,6 +135,20 @@ def parse_access(access_element):
uid = access_element.get("UId")
scope = access_element.get("Scope")
info = {"uid": uid, "scope": scope, "type": "unknown"}
# Manejo específico para direcciones (Address)
if scope == "Address":
address_elem = access_element.xpath("./flg:Address", namespaces=ns)
if address_elem:
addr = address_elem[0]
# Extraer toda la información disponible sobre la dirección
info["type"] = "unknown_structure" # Mantener compatible con el código existente
info["Area"] = addr.get("Area", "DB")
info["BitOffset"] = addr.get("BitOffset", "0")
info["BlockNumber"] = addr.get("BlockNumber", "")
info["Type"] = addr.get("Type", "Word") # Tipo por defecto: Word
return info
symbol = access_element.xpath("./flg:Symbol", namespaces=ns)
constant = access_element.xpath("./flg:Constant", namespaces=ns)
@ -248,7 +262,6 @@ def parse_access(access_element):
info["type"] = "error_no_name"
return info
def parse_part(part_element):
"""Parsea un nodo <flg:Part> de LAD/FBD."""
if part_element is None:

View File

@ -51,13 +51,21 @@ def process_call(instruction, network_id, sympy_map, symbol_manager: SymbolManag
break # Salir si una dependencia no está lista
# Convertir la expresión/constante a SCL para la llamada
# Simplificar ANTES de convertir? Probablemente no necesario para parámetros de entrada
# a menos que queramos optimizar el valor pasado. Por ahora, convertir directo.
param_scl_value = sympy_expr_to_scl(source_sympy_or_const, symbol_manager)
# Detectar si es una dirección indirecta (comienza con %)
is_address_ref = isinstance(source_sympy_or_const, str) and source_sympy_or_const.startswith("%")
# El nombre del pin SÍ necesita formateo
pin_name_scl = format_variable_name(pin_name)
scl_call_params.append(f"{pin_name_scl} := {param_scl_value}")
# Para direcciones indirectas, usar => en lugar de :=
if is_address_ref:
# Es un parámetro InOut con dirección indirecta
scl_call_params.append(f"{pin_name_scl} => {source_sympy_or_const}")
else:
# Es un parámetro normal (In)
param_scl_value = sympy_expr_to_scl(source_sympy_or_const, symbol_manager)
scl_call_params.append(f"{pin_name_scl} := {param_scl_value}")
processed_inputs.add(pin_name)
if not dependencies_resolved:
@ -121,7 +129,6 @@ def process_call(instruction, network_id, sympy_map, symbol_manager: SymbolManag
return True
# --- Processor Information Function ---
def get_processor_info():
"""Devuelve la información para las llamadas a FC y FB."""

View File

@ -47,6 +47,7 @@ def get_sympy_representation(source_info, network_id, sympy_map, symbol_manager)
# Handle single source dictionary
source_type = source_info.get("type")
source_scope = source_info.get("scope")
if source_type == "powerrail":
return sympy.true
@ -70,7 +71,6 @@ def get_sympy_representation(source_info, network_id, sympy_map, symbol_manager)
# Let's return their string value for now, processors will handle it.
# This might need refinement if constants need symbolic handling.
return str(value) # Or maybe symbol_manager.get_symbol(str(value))?
elif source_type == "connection":
map_key = (
network_id,
@ -79,6 +79,37 @@ def get_sympy_representation(source_info, network_id, sympy_map, symbol_manager)
)
# Return the SymPy object from the map
return sympy_map.get(map_key) # Returns None if not found (dependency not ready)
# Nueva lógica para manejar parámetros de tipo Address
elif source_type == "unknown_structure" and source_scope == "Address":
# Crear una representación especial para direcciones indirectas
area = source_info.get("Area", "DB") # DB por defecto
bit_offset = source_info.get("BitOffset", "0")
# Calcular byte y bit (para direcciones de bits)
try:
bit_offset_num = int(bit_offset)
byte_offset = bit_offset_num // 8
bit_in_byte = bit_offset_num % 8
# Generar referencia de dirección según el tipo
addr_type = source_info.get("Type", "Word") # Tipo por defecto: Word
if addr_type.upper() in ["WORD", "INT"]:
addr_ref = f"%{area}W{byte_offset}"
elif addr_type.upper() in ["DWORD", "DINT", "REAL"]:
addr_ref = f"%{area}D{byte_offset}"
elif addr_type.upper() in ["BYTE", "CHAR", "SINT"]:
addr_ref = f"%{area}B{byte_offset}"
elif addr_type.upper() in ["BOOL", "X"]:
addr_ref = f"%{area}X{byte_offset}.{bit_in_byte}"
else:
# Tipo no reconocido, usar Word como fallback
addr_ref = f"%{area}W{byte_offset}"
except ValueError:
# Si hay error en la conversión, usar un formato genérico
addr_ref = f"%{area}W{bit_offset}"
# Devolver la referencia como string - será manejada por process_call
return addr_ref
elif source_type == "unknown_source":
print(f"Warning: Referring to unknown source UID: {source_info.get('uid')}")
return None # Cannot resolve

View File

@ -15,5 +15,5 @@
"xref_source_subdir": "source"
},
"level3": {},
"working_directory": "C:\\Trabajo\\SIDEL\\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\\Reporte\\IOExport"
"working_directory": "C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\SourceDoc\\SourceXML"
}

View File

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

View File

@ -174,8 +174,20 @@ if __name__ == "__main__":
cfg_call_xref_filename = xml_parser_config.get("call_xref_filename", "xref_calls_tree.md")
cfg_db_usage_xref_filename = xml_parser_config.get("db_usage_xref_filename", "xref_db_usage_summary.md")
cfg_plc_tag_xref_filename = xml_parser_config.get("plc_tag_xref_filename", "xref_plc_tags_summary.md")
cfg_max_call_depth = xml_parser_config.get("max_call_depth", 5)
cfg_max_users_list = xml_parser_config.get("max_users_list", 20)
# Ensure max_call_depth is an integer
try:
cfg_max_call_depth = int(xml_parser_config.get("max_call_depth", 5))
except (ValueError, TypeError):
print("Advertencia: Valor inválido para 'max_call_depth' en la configuración. Usando valor por defecto 5.", file=sys.stderr)
cfg_max_call_depth = 5
# Ensure max_users_list is an integer
try:
cfg_max_users_list = int(xml_parser_config.get("max_users_list", 20))
except (ValueError, TypeError):
print("Advertencia: Valor inválido para 'max_users_list' en la configuración. Usando valor por defecto 20.", file=sys.stderr)
cfg_max_users_list = 20
cfg_aggregated_filename = xml_parser_config.get("aggregated_filename", "full_project_representation.md")
# <-- FIN NUEVO -->

View File

@ -0,0 +1,12 @@
{
"favorites": [
{
"id": "1_calc.py",
"group_id": "1",
"script_name": "calc.py",
"added_date": "2025-06-03T11:47:24.757186Z",
"execution_count": 0,
"last_executed": null
}
]
}

View File

@ -0,0 +1,9 @@
{
"history": [],
"settings": {
"max_entries": 100,
"auto_cleanup_days": 30,
"track_execution_time": true,
"track_arguments": true
}
}

View File

@ -0,0 +1,77 @@
{
"version": "1.0",
"groups": [
{
"id": "1",
"name": "Calculadora",
"description": "",
"category": "Herramientas",
"version": "1.0",
"directory": "D:/Proyectos/Scripts/Calcv2",
"author": "",
"tags": [],
"created_date": "2025-06-03T11:46:28.443910Z",
"updated_date": "2025-06-03T11:46:28.443910Z"
}
],
"categories": {
"Herramientas": {
"color": "#3B82F6",
"icon": "🔧",
"subcategories": [
"Generales",
"Desarrollo",
"Sistema"
]
},
"Análisis": {
"color": "#10B981",
"icon": "📊",
"subcategories": [
"Datos",
"Estadísticas",
"Visualización"
]
},
"Utilidades": {
"color": "#8B5CF6",
"icon": "⚙️",
"subcategories": [
"Archivos",
"Texto",
"Conversión"
]
},
"Desarrollo": {
"color": "#F59E0B",
"icon": "💻",
"subcategories": [
"Code",
"Testing",
"Deploy"
]
},
"Visualización": {
"color": "#EF4444",
"icon": "📈",
"subcategories": [
"Gráficos",
"Reportes",
"Dashboard"
]
},
"Otros": {
"color": "#6B7280",
"icon": "📁",
"subcategories": [
"Misceláneos"
]
}
},
"settings": {
"default_execution_directory": "script_directory",
"enable_argument_validation": true,
"max_history_entries": 100,
"auto_cleanup_days": 30
}
}

File diff suppressed because it is too large Load Diff

416
lib/launcher_manager.py Normal file
View File

@ -0,0 +1,416 @@
import os
import json
import subprocess
import sys
from typing import Dict, Any, List, Optional
from datetime import datetime
import uuid
class LauncherManager:
def __init__(self, data_path: str):
self.data_path = data_path
self.launcher_config_path = os.path.join(data_path, "launcher_scripts.json")
self.favorites_path = os.path.join(data_path, "launcher_favorites.json")
self.history_path = os.path.join(data_path, "launcher_history.json")
# Inicializar archivos si no existen
self._initialize_files()
def _initialize_files(self):
"""Crear archivos de configuración por defecto si no existen"""
# Inicializar launcher_scripts.json
if not os.path.exists(self.launcher_config_path):
default_config = {
"version": "1.0",
"groups": [],
"categories": {
"Herramientas": {
"color": "#3B82F6",
"icon": "🔧",
"subcategories": ["Generales", "Desarrollo", "Sistema"]
},
"Análisis": {
"color": "#10B981",
"icon": "📊",
"subcategories": ["Datos", "Estadísticas", "Visualización"]
},
"Utilidades": {
"color": "#8B5CF6",
"icon": "⚙️",
"subcategories": ["Archivos", "Texto", "Conversión"]
},
"Desarrollo": {
"color": "#F59E0B",
"icon": "💻",
"subcategories": ["Code", "Testing", "Deploy"]
},
"Visualización": {
"color": "#EF4444",
"icon": "📈",
"subcategories": ["Gráficos", "Reportes", "Dashboard"]
},
"Otros": {
"color": "#6B7280",
"icon": "📁",
"subcategories": ["Misceláneos"]
}
},
"settings": {
"default_execution_directory": "script_directory",
"enable_argument_validation": True,
"max_history_entries": 100,
"auto_cleanup_days": 30
}
}
with open(self.launcher_config_path, 'w', encoding='utf-8') as f:
json.dump(default_config, f, indent=2, ensure_ascii=False)
# Inicializar 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 launcher_history.json
if not os.path.exists(self.history_path):
default_history = {
"history": [],
"settings": {
"max_entries": 100,
"auto_cleanup_days": 30,
"track_execution_time": True,
"track_arguments": True
}
}
with open(self.history_path, 'w', encoding='utf-8') as f:
json.dump(default_history, f, indent=2, ensure_ascii=False)
def get_launcher_groups(self) -> List[Dict[str, Any]]:
"""Obtener todos los grupos de scripts GUI"""
try:
with open(self.launcher_config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
return config.get("groups", [])
except Exception as e:
print(f"Error loading launcher groups: {e}")
return []
def get_launcher_group(self, group_id: str) -> Optional[Dict[str, Any]]:
"""Obtener un grupo específico por ID"""
groups = self.get_launcher_groups()
for group in groups:
if group.get("id") == group_id:
return group
return None
def add_launcher_group(self, group_data: Dict[str, Any]) -> Dict[str, str]:
"""Agregar nuevo grupo de scripts GUI"""
try:
# Validar datos requeridos
required_fields = ["name", "directory"]
for field in required_fields:
if not group_data.get(field):
return {"status": "error", "message": f"Campo requerido: {field}"}
# Validar que el directorio existe
if not os.path.isdir(group_data["directory"]):
return {"status": "error", "message": "El directorio especificado no existe"}
# Generar ID único si no se proporciona
if not group_data.get("id"):
group_data["id"] = str(uuid.uuid4())[:8]
# Verificar que el ID no exista
if self.get_launcher_group(group_data["id"]):
return {"status": "error", "message": "Ya existe un grupo con este ID"}
# Agregar campos por defecto
current_time = datetime.now().isoformat() + "Z"
group_data.setdefault("description", "")
group_data.setdefault("category", "Otros")
group_data.setdefault("version", "1.0")
group_data.setdefault("author", "")
group_data.setdefault("tags", [])
group_data.setdefault("created_date", current_time)
group_data["updated_date"] = current_time
# Cargar configuración y agregar grupo
with open(self.launcher_config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
config["groups"].append(group_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": "Grupo agregado exitosamente"}
except Exception as e:
return {"status": "error", "message": f"Error agregando grupo: {str(e)}"}
def update_launcher_group(self, group_id: str, group_data: Dict[str, Any]) -> Dict[str, str]:
"""Actualizar grupo existente"""
try:
with open(self.launcher_config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
# Buscar y actualizar el grupo
group_found = False
for i, group in enumerate(config["groups"]):
if group["id"] == group_id:
# Mantener ID y fechas de creación
group_data["id"] = group_id
group_data["created_date"] = group.get("created_date", datetime.now().isoformat() + "Z")
group_data["updated_date"] = datetime.now().isoformat() + "Z"
config["groups"][i] = group_data
group_found = True
break
if not group_found:
return {"status": "error", "message": "Grupo 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": "Grupo actualizado exitosamente"}
except Exception as e:
return {"status": "error", "message": f"Error actualizando grupo: {str(e)}"}
def delete_launcher_group(self, group_id: str) -> Dict[str, str]:
"""Eliminar grupo de scripts GUI"""
try:
with open(self.launcher_config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
# Filtrar el grupo a eliminar
original_count = len(config["groups"])
config["groups"] = [g for g in config["groups"] if g["id"] != group_id]
if len(config["groups"]) == original_count:
return {"status": "error", "message": "Grupo no encontrado"}
with open(self.launcher_config_path, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False)
# Limpiar favoritos del grupo eliminado
self._cleanup_favorites_for_group(group_id)
return {"status": "success", "message": "Grupo eliminado exitosamente"}
except Exception as e:
return {"status": "error", "message": f"Error eliminando grupo: {str(e)}"}
def get_group_scripts(self, group_id: str) -> List[Dict[str, Any]]:
"""Obtener scripts de un grupo específico"""
try:
group = self.get_launcher_group(group_id)
if not group:
return []
directory = group["directory"]
if not os.path.isdir(directory):
return []
scripts = []
for file in os.listdir(directory):
if file.endswith('.py') and not file.startswith('_'):
script_path = os.path.join(directory, file)
if os.path.isfile(script_path):
scripts.append({
"name": file,
"display_name": file[:-3], # Sin extensión .py
"path": script_path,
"size": os.path.getsize(script_path),
"modified": datetime.fromtimestamp(os.path.getmtime(script_path)).isoformat()
})
return sorted(scripts, key=lambda x: x["name"])
except Exception as e:
print(f"Error getting scripts for group {group_id}: {e}")
return []
def execute_gui_script(self, group_id: str, script_name: str, script_args: List[str],
broadcast_func) -> Dict[str, Any]:
"""Ejecutar script GUI con argumentos opcionales"""
try:
group = self.get_launcher_group(group_id)
if not group:
return {"status": "error", "message": "Grupo no encontrado"}
script_path = os.path.join(group["directory"], script_name)
if not os.path.isfile(script_path):
return {"status": "error", "message": "Script no encontrado"}
# Construir comando
cmd = [sys.executable, script_path] + script_args
working_dir = group["directory"] # Por defecto directorio del script
broadcast_func(f"Ejecutando script GUI: {script_name}")
broadcast_func(f"Comando: {' '.join(cmd)}")
broadcast_func(f"Directorio: {working_dir}")
# Ejecutar script
start_time = datetime.now()
process = subprocess.Popen(
cmd,
cwd=working_dir,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
creationflags=subprocess.CREATE_NEW_CONSOLE if sys.platform == "win32" else 0
)
# Registrar en historial
execution_id = str(uuid.uuid4())[:8]
self._add_to_history({
"id": execution_id,
"group_id": group_id,
"script_name": script_name,
"executed_date": start_time.isoformat() + "Z",
"arguments": script_args,
"working_directory": working_dir,
"status": "running",
"pid": process.pid
})
broadcast_func(f"Script GUI ejecutado con PID: {process.pid}")
broadcast_func(f"ID de ejecución: {execution_id}")
return {
"status": "success",
"message": "Script GUI ejecutado exitosamente",
"execution_id": execution_id,
"pid": process.pid
}
except Exception as e:
error_msg = f"Error ejecutando script GUI: {str(e)}"
broadcast_func(error_msg)
return {"status": "error", "message": error_msg}
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 favorites: {e}")
return []
def toggle_favorite(self, group_id: str, script_name: str) -> Dict[str, str]:
"""Agregar o quitar script de favoritos"""
try:
with open(self.favorites_path, 'r', encoding='utf-8') as f:
data = json.load(f)
favorites = data.get("favorites", [])
favorite_id = f"{group_id}_{script_name}"
# Buscar si ya existe
existing_favorite = None
for i, fav in enumerate(favorites):
if fav["group_id"] == group_id and fav["script_name"] == script_name:
existing_favorite = i
break
if existing_favorite is not None:
# Quitar de favoritos
favorites.pop(existing_favorite)
action = "removed"
else:
# Agregar a favoritos
favorites.append({
"id": favorite_id,
"group_id": group_id,
"script_name": script_name,
"added_date": datetime.now().isoformat() + "Z",
"execution_count": 0,
"last_executed": None
})
action = "added"
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", "action": action}
except Exception as e:
return {"status": "error", "message": f"Error managing favorite: {str(e)}"}
def get_history(self) -> List[Dict[str, Any]]:
"""Obtener historial de ejecución"""
try:
with open(self.history_path, 'r', encoding='utf-8') as f:
data = json.load(f)
return data.get("history", [])
except Exception as e:
print(f"Error loading history: {e}")
return []
def clear_history(self) -> Dict[str, str]:
"""Limpiar historial de ejecución"""
try:
with open(self.history_path, 'r', encoding='utf-8') as f:
data = json.load(f)
data["history"] = []
with open(self.history_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return {"status": "success", "message": "Historial limpiado exitosamente"}
except Exception as e:
return {"status": "error", "message": f"Error clearing history: {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 categories: {e}")
return {}
def _add_to_history(self, entry: Dict[str, Any]):
"""Agregar entrada al historial"""
try:
with open(self.history_path, 'r', encoding='utf-8') as f:
data = json.load(f)
history = data.get("history", [])
history.insert(0, entry) # Agregar al inicio
# Limitar tamaño del historial
max_entries = data.get("settings", {}).get("max_entries", 100)
if len(history) > max_entries:
history = history[:max_entries]
data["history"] = history
with open(self.history_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Error adding to history: {e}")
def _cleanup_favorites_for_group(self, group_id: str):
"""Limpiar favoritos de un grupo eliminado"""
try:
with open(self.favorites_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# Filtrar favoritos del grupo eliminado
data["favorites"] = [f for f in data.get("favorites", []) if f.get("group_id") != group_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 up favorites for group {group_id}: {e}")

View File

@ -5,7 +5,7 @@
width: 400px;
height: 100vh;
background: white;
box-shadow: -2px 0 5px rgba(0,0,0,0.1);
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1);
transition: right 0.3s ease;
z-index: 40;
overflow-y: auto;
@ -21,7 +21,7 @@
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0,0,0,0.5);
background: rgba(0, 0, 0, 0.5);
display: none;
z-index: 30;
}
@ -106,23 +106,29 @@
/* Estilos para encabezados dentro de la descripción larga del script */
.long-description-content h1 {
font-size: 1.875rem; /* Equivalente a text-3xl de Tailwind */
font-size: 1.875rem;
/* Equivalente a text-3xl de Tailwind */
font-weight: bold;
margin-top: 1rem;
margin-bottom: 0.5rem;
}
.long-description-content h2 {
font-size: 1.5rem; /* Equivalente a text-2xl */
font-size: 1.5rem;
/* Equivalente a text-2xl */
font-weight: bold;
margin-top: 0.875rem;
margin-bottom: 0.4rem;
}
.long-description-content h3 {
font-size: 1.25rem; /* Equivalente a text-xl */
font-size: 1.25rem;
/* Equivalente a text-xl */
font-weight: bold;
margin-top: 0.75rem;
margin-bottom: 0.3rem;
}
/* Puedes añadir estilos para h4, h5, h6 si los necesitas */
.long-description-content hr {
@ -139,26 +145,392 @@
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.long-description-content ul {
list-style-type: disc;
}
.long-description-content ol {
list-style-type: decimal;
}
.long-description-content pre {
background-color: #f3f4f6; /* bg-gray-100 */
background-color: #f3f4f6;
/* bg-gray-100 */
padding: 0.75rem;
border-radius: 0.25rem;
overflow-x: auto;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.long-description-content code {
font-family: monospace;
/* Estilo para código en línea si es necesario */
}
/* Estilo específico para bloques de código dentro de <pre> */
.long-description-content pre code {
background-color: transparent;
padding: 0;
}
/* ===== LAUNCHER GUI STYLES ===== */
/* Tab Styles */
.tab-button {
color: #6B7280;
border-color: transparent;
transition: all 0.2s ease;
}
.tab-button:hover {
color: #374151;
border-color: #D1D5DB;
}
.tab-button.active {
color: #3B82F6;
border-color: #3B82F6;
}
.tab-content {
display: block;
}
.tab-content.hidden {
display: none;
}
/* Icon Styles */
.group-icon {
width: 48px;
height: 48px;
border-radius: 8px;
object-fit: cover;
border: 2px solid #E5E7EB;
}
.group-icon.default {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 20px;
}
.group-icon-small {
width: 24px;
height: 24px;
border-radius: 4px;
object-fit: cover;
border: 1px solid #E5E7EB;
}
.group-icon-small.default {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
}
/* Category Styles */
.category-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
background: #EFF6FF;
color: #1D4ED8;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
}
.category-btn {
background: #F9FAFB;
color: #6B7280;
border-color: #E5E7EB;
transition: all 0.2s ease;
}
.category-btn:hover {
background: #F3F4F6;
color: #374151;
border-color: #D1D5DB;
}
.category-btn.active {
background: #3B82F6;
color: white;
border-color: #3B82F6;
}
/* Script Card Styles */
.script-card {
border: 1px solid #E5E7EB;
border-radius: 8px;
padding: 1rem;
background: white;
transition: all 0.2s ease;
position: relative;
}
.script-card:hover {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
border-color: #D1D5DB;
}
.script-card.favorited {
border-color: #FCD34D;
background: #FFFBEB;
}
/* Favorite Star */
.favorite-star {
cursor: pointer;
transition: all 0.2s ease;
color: #D1D5DB;
}
.favorite-star:hover {
transform: scale(1.1);
color: #F59E0B;
}
.favorite-star.active {
color: #F59E0B;
}
/* History Item */
.history-item {
border: 1px solid #E5E7EB;
border-radius: 6px;
padding: 0.75rem;
background: #F9FAFB;
transition: all 0.2s ease;
}
.history-item:hover {
background: #F3F4F6;
border-color: #D1D5DB;
}
.history-item.success {
border-left: 4px solid #10B981;
}
.history-item.error {
border-left: 4px solid #EF4444;
}
.history-item.running {
border-left: 4px solid #3B82F6;
}
/* Favorites Panel */
.favorites-panel {
background: linear-gradient(135deg, #FEF3C7 0%, #FDE68A 100%);
}
.favorites-panel.empty {
display: none;
}
/* Group List Item */
.group-list-item {
display: flex;
align-items: center;
padding: 0.5rem;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
}
.group-list-item:hover {
background: #F3F4F6;
}
.group-list-item.selected {
background: #EBF8FF;
border: 1px solid #3B82F6;
}
.group-list-item .group-info {
margin-left: 0.75rem;
flex: 1;
}
.group-list-item .group-name {
font-weight: 500;
color: #374151;
}
.group-list-item .group-category {
font-size: 0.75rem;
color: #6B7280;
}
/* Modal Improvements */
.modal-content {
max-width: 90vw;
max-height: 90vh;
}
.modal-header {
border-bottom: 1px solid #E5E7EB;
padding: 1.5rem;
}
.modal-footer {
border-top: 1px solid #E5E7EB;
padding: 1.5rem;
}
/* Script Grid */
.scripts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
}
/* Tooltip */
.tooltip {
position: relative;
}
.tooltip:hover::after {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: #374151;
color: white;
padding: 0.5rem;
border-radius: 4px;
white-space: nowrap;
z-index: 1000;
font-size: 0.75rem;
}
/* Loading States */
.loading {
opacity: 0.6;
pointer-events: none;
}
.spinner {
display: inline-block;
width: 1rem;
height: 1rem;
border: 2px solid #E5E7EB;
border-radius: 50%;
border-top-color: #3B82F6;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Responsive Adjustments */
@media (max-width: 768px) {
.scripts-grid {
grid-template-columns: 1fr;
}
.tab-button span {
font-size: 0.75rem;
}
.tab-button svg {
width: 1rem;
height: 1rem;
}
}
/* Animation Classes */
.fade-in {
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.slide-in {
animation: slideIn 0.3s ease-in-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* Status Indicators */
.status-success {
color: #10B981;
}
.status-error {
color: #EF4444;
}
.status-running {
color: #3B82F6;
}
.status-warning {
color: #F59E0B;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 3rem 1rem;
color: #6B7280;
}
.empty-state svg {
width: 4rem;
height: 4rem;
margin: 0 auto 1rem;
color: #D1D5DB;
}
/* Button Variants */
.btn-outline {
border: 1px solid currentColor;
background: transparent;
transition: all 0.2s ease;
}
.btn-outline:hover {
background: currentColor;
color: white;
}
/* Focus States */
.focus-visible:focus {
outline: 2px solid #3B82F6;
outline-offset: 2px;
}

709
static/js/launcher.js Normal file
View File

@ -0,0 +1,709 @@
// launcher.js - Funcionalidad del Launcher GUI
class LauncherManager {
constructor() {
this.currentGroup = null;
this.groups = [];
this.scripts = [];
this.favorites = new Set();
this.history = [];
this.categories = {};
this.currentFilter = 'all';
this.currentEditingGroup = null;
}
async init() {
console.log('Inicializando Launcher GUI...');
await this.loadCategories();
await this.loadGroups();
await this.loadFavorites();
await this.loadHistory();
this.setupEventListeners();
this.renderInterface();
}
async loadCategories() {
try {
const response = await fetch('/api/launcher-categories');
this.categories = await response.json();
} catch (error) {
console.error('Error loading launcher categories:', error);
}
}
async loadGroups() {
try {
const response = await fetch('/api/launcher-groups');
this.groups = await response.json();
this.renderGroupSelector();
} catch (error) {
console.error('Error loading launcher groups:', error);
}
}
async loadFavorites() {
try {
const response = await fetch('/api/launcher-favorites');
const data = await response.json();
this.favorites = new Set(data.favorites.map(f => `${f.group_id}_${f.script_name}`));
this.renderFavorites(data.favorites);
} catch (error) {
console.error('Error loading favorites:', error);
}
}
async loadHistory() {
try {
const response = await fetch('/api/launcher-history');
const data = await response.json();
this.history = data.history || [];
this.renderHistory();
} catch (error) {
console.error('Error loading history:', error);
}
}
setupEventListeners() {
// Event listener para el formulario de grupos
const groupForm = document.getElementById('group-form');
if (groupForm) {
groupForm.addEventListener('submit', (e) => {
e.preventDefault();
this.saveGroup();
});
}
}
renderInterface() {
this.renderGroupSelector();
this.renderCategoryFilter();
this.updateFavoritesCount();
}
renderGroupSelector() {
const selector = document.getElementById('launcher-group-select');
if (!selector) return;
selector.innerHTML = '<option value="">-- Seleccionar Grupo --</option>';
this.groups.forEach(group => {
const option = document.createElement('option');
option.value = group.id;
option.textContent = group.name;
option.dataset.category = group.category;
option.dataset.description = group.description;
selector.appendChild(option);
});
}
renderCategoryFilter() {
const filterContainer = document.querySelector('.category-filter .flex');
if (!filterContainer) return;
// Limpiar botones existentes excepto "Todas"
const buttons = filterContainer.querySelectorAll('.category-btn:not([data-category="all"])');
buttons.forEach(btn => btn.remove());
// Agregar botones por categoría
Object.keys(this.categories).forEach(category => {
const categoryData = this.categories[category];
const button = document.createElement('button');
button.className = 'category-btn px-3 py-1 rounded-full text-sm border';
button.dataset.category = category;
button.innerHTML = `${categoryData.icon} ${category}`;
button.onclick = () => this.filterByCategory(category);
filterContainer.appendChild(button);
});
}
async loadLauncherScripts() {
const groupId = document.getElementById('launcher-group-select').value;
if (!groupId) {
this.scripts = [];
this.renderScripts();
return;
}
try {
const response = await fetch(`/api/launcher-scripts/${groupId}`);
this.scripts = await response.json();
this.currentGroup = this.groups.find(g => g.id === groupId);
this.updateGroupIcon();
this.renderScripts();
} catch (error) {
console.error('Error loading launcher scripts:', error);
this.scripts = [];
this.renderScripts();
}
}
updateGroupIcon() {
const iconElement = document.getElementById('selected-group-icon');
if (!iconElement || !this.currentGroup) return;
// Intentar cargar icono personalizado
const img = document.createElement('img');
img.src = `/api/group-icon/launcher/${this.currentGroup.id}`;
img.className = 'w-6 h-6 rounded';
img.onerror = () => {
// Fallback a icono por defecto
iconElement.innerHTML = this.getDefaultIconForCategory(this.currentGroup.category);
};
img.onload = () => {
iconElement.innerHTML = '';
iconElement.appendChild(img);
};
}
getDefaultIconForCategory(category) {
const icons = {
'Herramientas': '🔧',
'Análisis': '📊',
'Utilidades': '⚙️',
'Desarrollo': '💻',
'Visualización': '📈',
'Otros': '📁'
};
return icons[category] || '📁';
}
renderScripts() {
const grid = document.getElementById('launcher-scripts-grid');
if (!grid) return;
if (this.scripts.length === 0) {
grid.innerHTML = `
<div class="col-span-full empty-state">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<p class="text-lg font-medium">No hay scripts disponibles</p>
<p class="text-sm">Selecciona un grupo o verifica que el directorio contenga archivos .py</p>
</div>
`;
return;
}
let filteredScripts = this.scripts;
if (this.currentFilter !== 'all' && this.currentGroup) {
if (this.currentGroup.category !== this.currentFilter) {
filteredScripts = [];
}
}
grid.innerHTML = '';
filteredScripts.forEach(script => {
const favoriteId = `${this.currentGroup.id}_${script.name}`;
const isFavorite = this.favorites.has(favoriteId);
const card = document.createElement('div');
card.className = `script-card ${isFavorite ? 'favorited' : ''}`;
card.innerHTML = `
<div class="flex justify-between items-start mb-2">
<h4 class="font-medium text-gray-900">${script.display_name}</h4>
<button class="favorite-star ${isFavorite ? 'active' : ''}"
onclick="launcherManager.toggleFavorite('${this.currentGroup.id}', '${script.name}')">
</button>
</div>
<p class="text-sm text-gray-600 mb-3 line-clamp-2">Script: ${script.name}</p>
<div class="flex justify-between items-center">
<span class="category-badge">${this.currentGroup.category}</span>
<div class="space-x-2">
<button class="text-blue-500 hover:underline text-sm"
onclick="launcherManager.showArgsModal('${script.name}', '${script.display_name}')">
Con Argumentos
</button>
<button class="bg-blue-500 text-white px-3 py-1 rounded text-sm hover:bg-blue-600"
onclick="launcherManager.executeScript('${script.name}')">
Ejecutar
</button>
</div>
</div>
`;
grid.appendChild(card);
});
}
filterByCategory(category) {
this.currentFilter = category;
// Actualizar botones activos
document.querySelectorAll('.category-btn').forEach(btn => {
btn.classList.remove('active');
});
document.querySelector(`[data-category="${category}"]`).classList.add('active');
// Filtrar grupos en el selector
const selector = document.getElementById('launcher-group-select');
Array.from(selector.options).forEach(option => {
if (option.value === '') return;
option.style.display = (category === 'all' || option.dataset.category === category) ? '' : 'none';
});
// Re-renderizar scripts si hay grupo seleccionado
if (this.currentGroup) {
this.renderScripts();
}
}
async toggleFavorite(groupId, scriptName) {
try {
const response = await fetch('/api/launcher-favorites', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
group_id: groupId,
script_name: scriptName
})
});
const result = await response.json();
if (result.status === 'success') {
const favoriteId = `${groupId}_${scriptName}`;
if (result.action === 'added') {
this.favorites.add(favoriteId);
} else {
this.favorites.delete(favoriteId);
}
// Recargar datos y re-renderizar
await this.loadFavorites();
this.renderScripts();
this.updateFavoritesCount();
}
} catch (error) {
console.error('Error toggling favorite:', error);
}
}
async executeScript(scriptName, args = []) {
if (!this.currentGroup) return;
try {
const response = await fetch('/api/execute-gui-script', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
group_id: this.currentGroup.id,
script_name: scriptName,
args: args
})
});
const result = await response.json();
if (result.status === 'success') {
// Recargar historial
await this.loadHistory();
}
} catch (error) {
console.error('Error executing script:', error);
}
}
showArgsModal(scriptName, displayName) {
const modal = document.getElementById('script-args-modal');
const scriptDisplayElement = document.getElementById('script-display-name');
const argsInput = document.getElementById('script-args-input');
if (modal && scriptDisplayElement && argsInput) {
scriptDisplayElement.textContent = displayName;
argsInput.value = '';
modal.classList.remove('hidden');
// Guardar datos para uso posterior
modal.dataset.scriptName = scriptName;
modal.dataset.groupId = this.currentGroup.id;
}
}
renderFavorites(favorites) {
const favoritesList = document.getElementById('favorites-list');
const favoritesPanel = document.getElementById('favorites-panel');
if (!favoritesList || !favoritesPanel) return;
if (favorites.length === 0) {
favoritesPanel.classList.add('empty');
return;
}
favoritesPanel.classList.remove('empty');
favoritesList.innerHTML = '';
favorites.slice(0, 5).forEach(fav => {
const group = this.groups.find(g => g.id === fav.group_id);
if (!group) return;
const item = document.createElement('div');
item.className = 'flex items-center justify-between p-2 bg-white rounded border';
item.innerHTML = `
<div class="flex items-center">
<div class="group-icon-small default mr-2">
${this.getDefaultIconForCategory(group.category)}
</div>
<div>
<div class="font-medium text-sm">${fav.script_name.replace('.py', '')}</div>
<div class="text-xs text-gray-500">${group.name}</div>
</div>
</div>
<button class="text-blue-500 hover:underline text-sm"
onclick="launcherManager.executeFavoriteScript('${fav.group_id}', '${fav.script_name}')">
Ejecutar
</button>
`;
favoritesList.appendChild(item);
});
}
async executeFavoriteScript(groupId, scriptName) {
// Cambiar al grupo correcto si no está seleccionado
if (!this.currentGroup || this.currentGroup.id !== groupId) {
document.getElementById('launcher-group-select').value = groupId;
await this.loadLauncherScripts();
}
this.executeScript(scriptName);
}
renderHistory() {
const historyList = document.getElementById('history-list');
if (!historyList) return;
if (this.history.length === 0) {
historyList.innerHTML = `
<div class="text-center text-gray-500 py-4">
<p>No hay ejecuciones recientes</p>
</div>
`;
return;
}
historyList.innerHTML = '';
this.history.slice(0, 10).forEach(entry => {
const group = this.groups.find(g => g.id === entry.group_id);
const groupName = group ? group.name : 'Grupo desconocido';
const timeAgo = this.getTimeAgo(entry.executed_date);
const statusClass = entry.status === 'success' ? 'success' :
entry.status === 'error' ? 'error' : 'running';
const statusIcon = entry.status === 'success' ? '✅' :
entry.status === 'error' ? '❌' : '🔄';
const item = document.createElement('div');
item.className = `history-item ${statusClass}`;
item.innerHTML = `
<div class="flex justify-between items-start">
<div>
<span class="font-medium">${entry.script_name.replace('.py', '')}</span>
<span class="text-sm text-gray-500 ml-2">${groupName}</span>
</div>
<span class="text-xs text-gray-400">${timeAgo}</span>
</div>
<div class="text-sm text-gray-600 mt-1">
${statusIcon} ${entry.status.charAt(0).toUpperCase() + entry.status.slice(1)}
${entry.execution_time ? ` - ${entry.execution_time}s` : ''}
${entry.arguments && entry.arguments.length > 0 ? ` - Con argumentos` : ''}
</div>
`;
historyList.appendChild(item);
});
}
getTimeAgo(dateString) {
const date = new Date(dateString);
const now = new Date();
const diffMs = now - date;
const diffMinutes = Math.floor(diffMs / 60000);
if (diffMinutes < 1) return 'ahora';
if (diffMinutes < 60) return `hace ${diffMinutes}m`;
if (diffMinutes < 1440) return `hace ${Math.floor(diffMinutes / 60)}h`;
return `hace ${Math.floor(diffMinutes / 1440)}d`;
}
updateFavoritesCount() {
const countElement = document.getElementById('favorites-count');
if (countElement) {
countElement.textContent = `${this.favorites.size} favoritos`;
}
}
async clearLauncherHistory() {
if (!confirm('¿Estás seguro de que quieres limpiar el historial?')) return;
try {
const response = await fetch('/api/launcher-history', {
method: 'DELETE'
});
const result = await response.json();
if (result.status === 'success') {
this.history = [];
this.renderHistory();
}
} catch (error) {
console.error('Error clearing history:', error);
}
}
// === GESTIÓN DE GRUPOS ===
openGroupEditor() {
const modal = document.getElementById('group-editor-modal');
if (modal) {
this.currentEditingGroup = null;
this.clearGroupForm();
this.renderExistingGroups();
modal.classList.remove('hidden');
}
}
closeGroupEditor() {
const modal = document.getElementById('group-editor-modal');
if (modal) {
modal.classList.add('hidden');
this.currentEditingGroup = null;
}
}
renderExistingGroups() {
const container = document.getElementById('existing-groups-list');
if (!container) return;
container.innerHTML = '';
this.groups.forEach(group => {
const item = document.createElement('div');
item.className = 'group-list-item';
item.innerHTML = `
<div class="group-icon-small default">
${this.getDefaultIconForCategory(group.category)}
</div>
<div class="group-info">
<div class="group-name">${group.name}</div>
<div class="group-category">${group.category}</div>
</div>
<button class="text-blue-500 hover:underline text-sm" onclick="launcherManager.editGroup('${group.id}')">
Editar
</button>
`;
container.appendChild(item);
});
}
editGroup(groupId) {
const group = this.groups.find(g => g.id === groupId);
if (!group) return;
this.currentEditingGroup = group;
this.populateGroupForm(group);
document.getElementById('delete-group-btn').style.display = 'block';
}
populateGroupForm(group) {
document.getElementById('group-id').value = group.id;
document.getElementById('group-name').value = group.name;
document.getElementById('group-description').value = group.description || '';
document.getElementById('group-category').value = group.category;
document.getElementById('group-version').value = group.version || '1.0';
document.getElementById('group-directory').value = group.directory;
}
clearGroupForm() {
document.getElementById('group-id').value = '';
document.getElementById('group-name').value = '';
document.getElementById('group-description').value = '';
document.getElementById('group-category').value = 'Otros';
document.getElementById('group-version').value = '1.0';
document.getElementById('group-directory').value = '';
document.getElementById('delete-group-btn').style.display = 'none';
}
async saveGroup() {
const formData = {
id: document.getElementById('group-id').value,
name: document.getElementById('group-name').value,
description: document.getElementById('group-description').value,
category: document.getElementById('group-category').value,
version: document.getElementById('group-version').value,
directory: document.getElementById('group-directory').value
};
try {
let response;
if (this.currentEditingGroup) {
// Actualizar grupo existente
response = await fetch(`/api/launcher-groups/${this.currentEditingGroup.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
});
} else {
// Crear nuevo grupo
response = await fetch('/api/launcher-groups', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
});
}
const result = await response.json();
if (result.status === 'success') {
await this.loadGroups();
this.closeGroupEditor();
this.renderInterface();
} else {
alert(`Error: ${result.message}`);
}
} catch (error) {
console.error('Error saving group:', error);
alert('Error al guardar el grupo');
}
}
async deleteGroup() {
if (!this.currentEditingGroup) return;
if (!confirm(`¿Estás seguro de que quieres eliminar el grupo "${this.currentEditingGroup.name}"?`)) return;
try {
const response = await fetch(`/api/launcher-groups/${this.currentEditingGroup.id}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.status === 'success') {
await this.loadGroups();
this.closeGroupEditor();
this.renderInterface();
// Limpiar selección si era el grupo actual
if (this.currentGroup && this.currentGroup.id === this.currentEditingGroup.id) {
document.getElementById('launcher-group-select').value = '';
this.currentGroup = null;
this.scripts = [];
this.renderScripts();
}
} else {
alert(`Error: ${result.message}`);
}
} catch (error) {
console.error('Error deleting group:', error);
alert('Error al eliminar el grupo');
}
}
browseGroupDirectory() {
// Similar a la función existente pero para el formulario de grupos
fetch('/api/browse-directories')
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
document.getElementById('group-directory').value = data.path;
}
})
.catch(error => {
console.error('Error browsing directory:', error);
});
}
}
// === FUNCIONES GLOBALES ===
// Función para cambiar entre tabs
function switchTab(tabName) {
// Cambiar tabs activos
document.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.remove('active');
});
document.getElementById(`${tabName}-tab`).classList.add('active');
// Cambiar contenido
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.add('hidden');
});
document.getElementById(`${tabName}-content`).classList.remove('hidden');
// Inicializar launcher si es la primera vez
if (tabName === 'launcher' && !window.launcherManager) {
window.launcherManager = new LauncherManager();
window.launcherManager.init();
}
}
// Funciones para modales
function openGroupEditor() {
if (window.launcherManager) {
window.launcherManager.openGroupEditor();
}
}
function closeGroupEditor() {
if (window.launcherManager) {
window.launcherManager.closeGroupEditor();
}
}
function deleteGroup() {
if (window.launcherManager) {
window.launcherManager.deleteGroup();
}
}
function browseGroupDirectory() {
if (window.launcherManager) {
window.launcherManager.browseGroupDirectory();
}
}
function filterByCategory(category) {
if (window.launcherManager) {
window.launcherManager.filterByCategory(category);
}
}
function loadLauncherScripts() {
if (window.launcherManager) {
window.launcherManager.loadLauncherScripts();
}
}
function clearLauncherHistory() {
if (window.launcherManager) {
window.launcherManager.clearLauncherHistory();
}
}
// Funciones para modal de argumentos
function closeArgsModal() {
const modal = document.getElementById('script-args-modal');
if (modal) {
modal.classList.add('hidden');
}
}
function executeWithArgs() {
const modal = document.getElementById('script-args-modal');
const argsInput = document.getElementById('script-args-input');
if (modal && argsInput && window.launcherManager) {
const scriptName = modal.dataset.scriptName;
const args = argsInput.value.trim().split(/\s+/).filter(arg => arg.length > 0);
window.launcherManager.executeScript(scriptName, args);
closeArgsModal();
}
}
// Inicialización cuando se carga la página
document.addEventListener('DOMContentLoaded', function () {
console.log('Launcher JS loaded');
});

View File

@ -1,5 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -7,13 +8,17 @@
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
</head>
<body class="bg-gray-100">
<!-- Settings Button -->
<div class="fixed top-4 right-4 z-50">
<button onclick="toggleSidebar()" class="bg-white p-2 rounded-full shadow-lg hover:bg-gray-100">
<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="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z">
</path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
</button>
</div>
@ -26,7 +31,8 @@
<h2 class="text-xl font-bold">Configuración Global</h2>
<button onclick="toggleSidebar()" 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>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12">
</path>
</svg>
</button>
</div>
@ -35,8 +41,7 @@
<div class="mb-8">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Configuración Base</h2>
<button class="bg-blue-500 text-white px-4 py-2 rounded"
onclick="toggleConfig('level1-content')">
<button class="bg-blue-500 text-white px-4 py-2 rounded" onclick="toggleConfig('level1-content')">
Mostrar Configuración
</button>
</div>
@ -53,9 +58,8 @@
<!-- Level 2 Configuration -->
<div class="mb-8">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Configuración del Scrips</h2>
<button class="bg-blue-500 text-white px-4 py-2 rounded"
onclick="toggleConfig('level2-content')">
<h2 class="text-xl font-bold">Configuración del Scripts</h2>
<button class="bg-blue-500 text-white px-4 py-2 rounded" onclick="toggleConfig('level2-content')">
Mostrar Configuración
</button>
</div>
@ -71,10 +75,12 @@
<!-- Botón para detener el servidor -->
<div class="mt-8 pt-4 border-t border-gray-300">
<button class="w-full bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded shadow mb-2" onclick="openMinicondaConsole()">
<button class="w-full bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded shadow mb-2"
onclick="openMinicondaConsole()">
Abrir Miniconda Console
</button>
<button class="w-full bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded shadow" onclick="shutdownServer()">
<button class="w-full bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded shadow"
onclick="shutdownServer()">
Detener Servidor
</button>
</div>
@ -83,84 +89,214 @@
<!-- Main Content -->
<div class="container mx-auto px-4 py-8">
<!-- Script Group Selection -->
<div class="mb-8 bg-white p-6 rounded-lg shadow">
<h2 class="text-xl font-bold mb-4">Funciones a utilizar:</h2>
<div class="flex gap-2 items-center">
<select id="script-group" class="flex-1 p-2 border rounded mb-2">
{% for group in script_groups %}
<option value="{{ group.id }}" data-description="{{ group.description }}">{{ group.name }}</option>
{% endfor %}
</select>
<button onclick="editGroupDescription()" class="bg-blue-500 text-white p-2 rounded mb-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
</button>
<button onclick="openGroupInVsCode()" class="bg-blue-500 text-white p-2 rounded mb-2" title="Abrir grupo en VS Code">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
</button>
</div>
<p id="group-description" class="text-gray-600 text-sm italic"></p>
<div class="text-xs text-gray-500 mt-2">
<span id="group-version"></span>
<span id="group-author"></span>
<!-- Tab Navigation -->
<div class="mb-8">
<div class="border-b border-gray-200">
<nav class="-mb-px flex space-x-8">
<button id="config-tab" onclick="switchTab('config')"
class="tab-button active 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="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z">
</path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
Scripts de Configuración
</span>
</button>
<button id="launcher-tab" onclick="switchTab('launcher')"
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="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
</span>
</button>
</nav>
</div>
</div>
<!-- Working Directory -->
<div class="mb-8 bg-white p-6 rounded-lg shadow">
<h2 class="text-xl font-bold mb-4">Directorio de Trabajo</h2>
<div class="flex gap-4">
<div class="flex-1 flex gap-2">
<input type="text" id="working-directory" class="flex-1 p-2 border rounded bg-green-50">
<button class="bg-gray-500 text-white px-4 py-2 rounded" onclick="browseDirectory()">
Explorar
<!-- Tab Content: Sistema de Configuración Actual -->
<div id="config-content" class="tab-content">
<!-- Script Group Selection -->
<div class="mb-8 bg-white p-6 rounded-lg shadow">
<h2 class="text-xl font-bold mb-4">Funciones a utilizar:</h2>
<div class="flex gap-2 items-center">
<select id="script-group" class="flex-1 p-2 border rounded mb-2">
{% for group in script_groups %}
<option value="{{ group.id }}" data-description="{{ group.description }}">{{ group.name }}
</option>
{% endfor %}
</select>
<button onclick="editGroupDescription()" class="bg-blue-500 text-white p-2 rounded mb-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z">
</path>
</svg>
</button>
<button id="open-in-explorer-btn" class="bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded" title="Abrir directorio actual en el explorador de archivos">
Abrir Carpeta
<button onclick="openGroupInVsCode()" class="bg-blue-500 text-white p-2 rounded mb-2"
title="Abrir grupo en VS Code">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
</button>
</div>
<button class="bg-blue-500 text-white px-4 py-2 rounded" onclick="setWorkingDirectory()">
Confirmar
</button>
<p id="group-description" class="text-gray-600 text-sm italic"></p>
<div class="text-xs text-gray-500 mt-2">
<span id="group-version"></span>
<span id="group-author"></span>
</div>
</div>
<!-- Add directory history dropdown -->
<div class="mt-2">
<select id="directory-history" class="w-full p-2 border rounded text-gray-600" onchange="loadHistoryDirectory(this.value)">
<option value="">-- Directorios recientes --</option>
</select>
<!-- Working Directory -->
<div class="mb-8 bg-white p-6 rounded-lg shadow">
<h2 class="text-xl font-bold mb-4">Directorio de Trabajo</h2>
<div class="flex gap-4">
<div class="flex-1 flex gap-2">
<input type="text" id="working-directory" class="flex-1 p-2 border rounded bg-green-50">
<button class="bg-gray-500 text-white px-4 py-2 rounded" onclick="browseDirectory()">
Explorar
</button>
<button id="open-in-explorer-btn"
class="bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded"
title="Abrir directorio actual en el explorador de archivos">
Abrir Carpeta
</button>
</div>
<button class="bg-blue-500 text-white px-4 py-2 rounded" onclick="setWorkingDirectory()">
Confirmar
</button>
</div>
<!-- Add directory history dropdown -->
<div class="mt-2">
<select id="directory-history" class="w-full p-2 border rounded text-gray-600"
onchange="loadHistoryDirectory(this.value)">
<option value="">-- Directorios recientes --</option>
</select>
</div>
</div>
<!-- Level 3 Configuration -->
<div class="mb-8 bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Configuración del Directorio</h2>
<button class="bg-blue-500 text-white px-4 py-2 rounded" onclick="toggleConfig('level3-content')">
Ocultar Configuración
</button>
</div>
<div id="level3-content">
<div id="level3-form"></div>
<div class="flex justify-end mt-4">
<button class="bg-blue-500 text-white px-4 py-2 rounded" onclick="modifySchema(3)">
Modificar Esquema
</button>
</div>
</div>
</div>
<!-- Scripts List -->
<div class="mb-8 bg-white p-6 rounded-lg shadow">
<h2 class="text-xl font-bold mb-4">Scripts Disponibles</h2>
<div id="scripts-list" class="space-y-4"></div>
</div>
</div>
<!-- Level 3 Configuration -->
<div class="mb-8 bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Configuración del Directorio</h2>
<button class="bg-blue-500 text-white px-4 py-2 rounded"
onclick="toggleConfig('level3-content')">
Ocultar Configuración
</button>
</div>
<div id="level3-content">
<div id="level3-form"></div>
<div class="flex justify-end mt-4">
<button class="bg-blue-500 text-white px-4 py-2 rounded" onclick="modifySchema(3)">
Modificar Esquema
<!-- Tab Content: Nuevo Launcher GUI -->
<div id="launcher-content" class="tab-content hidden">
<!-- 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>
<button onclick="openGroupEditor()"
class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
Gestionar Grupos
</button>
</div>
<!-- Group Selector -->
<div class="mb-4">
<label class="block text-sm font-medium mb-2">Seleccionar Grupo de Scripts</label>
<div class="relative">
<select id="launcher-group-select" class="w-full p-3 border rounded-lg pl-12"
onchange="loadLauncherScripts()">
<option value="">-- Seleccionar Grupo --</option>
</select>
<div class="absolute left-3 top-1/2 transform -translate-y-1/2">
<div id="selected-group-icon"
class="w-6 h-6 bg-gray-200 rounded flex items-center justify-center text-sm">📁</div>
</div>
</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="category-btn active px-3 py-1 rounded-full text-sm border" data-category="all"
onclick="filterByCategory('all')">
Todas
</button>
<button class="category-btn px-3 py-1 rounded-full text-sm border" data-category="Herramientas"
onclick="filterByCategory('Herramientas')">
🔧 Herramientas
</button>
<button class="category-btn px-3 py-1 rounded-full text-sm border" data-category="Análisis"
onclick="filterByCategory('Análisis')">
📊 Análisis
</button>
<button class="category-btn px-3 py-1 rounded-full text-sm border" data-category="Utilidades"
onclick="filterByCategory('Utilidades')">
⚙️ Utilidades
</button>
</div>
</div>
</div>
<!-- Favorites Panel -->
<div id="favorites-panel" class="mb-6 bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div class="flex items-center justify-between mb-3">
<h3 class="text-lg font-semibold text-yellow-800">
⭐ Scripts Favoritos
</h3>
<span class="text-sm text-yellow-600" id="favorites-count">
0 favoritos
</span>
</div>
<div id="favorites-list" class="space-y-2">
<!-- Lista dinámica de favoritos -->
</div>
</div>
<!-- Scripts Grid -->
<div class="mb-6 bg-white p-6 rounded-lg shadow">
<h2 class="text-xl font-bold mb-4">Scripts Disponibles</h2>
<div id="launcher-scripts-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Scripts cards dinámicos -->
</div>
</div>
<!-- History 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">📝 Historial Reciente</h3>
<button onclick="clearLauncherHistory()" class="text-red-500 hover:text-red-700 text-sm">
Limpiar Historial
</button>
</div>
<div id="history-list" class="space-y-2 max-h-64 overflow-y-auto">
<!-- Lista dinámica de historial -->
</div>
</div>
</div>
<!-- Scripts List -->
<div class="mb-8 bg-white p-6 rounded-lg shadow">
<h2 class="text-xl font-bold mb-4">Scripts Disponibles</h2>
<div id="scripts-list" class="space-y-4"></div> <!-- Añadido space-y-4 para separación -->
</div>
<!-- Logs -->
<!-- Logs (común para ambos sistemas) -->
<div class="bg-white p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Logs</h2>
@ -168,12 +304,13 @@
Limpiar
</button>
</div>
<div id="log-area" class="bg-gray-100 p-4 rounded h-64 overflow-y-auto font-mono text-sm whitespace-pre-wrap">
<div id="log-area"
class="bg-gray-100 p-4 rounded h-64 overflow-y-auto font-mono text-sm whitespace-pre-wrap">
</div>
</div>
</div>
<!-- Schema Editor Modal -->
<!-- Schema Editor Modal (existente) -->
<div id="schema-editor" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center">
<div class="modal-content bg-white rounded-lg shadow-lg max-h-screen overflow-auto">
<div class="modal-header">
@ -184,14 +321,14 @@
class="bg-gray-500 text-white px-4 py-2 rounded">
Cancelar
</button>
<button onclick="saveSchema()"
class="bg-blue-500 text-white px-4 py-2 rounded">
<button onclick="saveSchema()" class="bg-blue-500 text-white px-4 py-2 rounded">
Guardar
</button>
</div>
</div>
<div class="flex mt-4" id="editor-tabs">
<button id="visual-tab" class="px-4 py-2 border-b-2" onclick="switchEditorMode('visual')">Visual</button>
<button id="visual-tab" class="px-4 py-2 border-b-2"
onclick="switchEditorMode('visual')">Visual</button>
<button id="json-tab" class="px-4 py-2 border-b-2" onclick="switchEditorMode('json')">JSON</button>
</div>
</div>
@ -199,7 +336,8 @@
<div class="p-4">
<div id="visual-editor" class="hidden">
<div id="schema-fields"></div>
<button onclick="addSchemaField()" class="mt-4 bg-green-500 text-white px-4 py-2 rounded">Agregar Campo</button>
<button onclick="addSchemaField()" class="mt-4 bg-green-500 text-white px-4 py-2 rounded">Agregar
Campo</button>
</div>
<textarea id="json-editor" class="w-full h-96 font-mono p-2 border rounded"></textarea>
<input type="hidden" id="schema-level">
@ -211,8 +349,7 @@
class="bg-gray-500 text-white px-4 py-2 rounded">
Cancelar
</button>
<button onclick="saveSchema()"
class="bg-blue-500 text-white px-4 py-2 rounded">
<button onclick="saveSchema()" class="bg-blue-500 text-white px-4 py-2 rounded">
Guardar
</button>
</div>
@ -220,13 +357,15 @@
</div>
</div>
<!-- Script Details Editor Modal -->
<div id="script-editor-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center z-50">
<!-- Script Details Editor Modal (existente) -->
<div id="script-editor-modal"
class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center z-50">
<div class="modal-content bg-white rounded-lg shadow-lg w-full max-w-lg max-h-[90vh] overflow-auto">
<div class="modal-header sticky top-0 bg-white border-b p-4">
<div class="flex justify-between items-center">
<h3 class="text-xl font-bold">Editar Detalles del Script</h3>
<button onclick="closeScriptEditorModal()" class="text-gray-500 hover:text-gray-700">&times;</button>
<button onclick="closeScriptEditorModal()"
class="text-gray-500 hover:text-gray-700">&times;</button>
</div>
</div>
<div class="p-6 space-y-4">
@ -234,7 +373,8 @@
<input type="hidden" id="edit-script-filename">
<div>
<label class="block text-sm font-bold mb-1">Nombre del Archivo</label>
<p id="edit-script-filename-display" class="text-sm text-gray-600 bg-gray-100 p-2 rounded border"></p>
<p id="edit-script-filename-display" class="text-sm text-gray-600 bg-gray-100 p-2 rounded border">
</p>
</div>
<div>
@ -242,29 +382,155 @@
<input type="text" id="edit-script-display-name" class="w-full p-2 border rounded">
</div>
<div>
<label for="edit-script-short-description" class="block text-sm font-bold mb-2">Descripción Corta</label>
<label for="edit-script-short-description" class="block text-sm font-bold mb-2">Descripción
Corta</label>
<input type="text" id="edit-script-short-description" class="w-full p-2 border rounded">
</div>
<div>
<label for="edit-script-long-description" class="block text-sm font-bold mb-2">Descripción Larga / Ayuda</label>
<label for="edit-script-long-description" class="block text-sm font-bold mb-2">Descripción Larga /
Ayuda</label>
<textarea id="edit-script-long-description" class="w-full p-2 border rounded" rows="5"></textarea>
<p class="text-xs text-gray-500 mt-1">Usa Markdown. Doble Enter para párrafo nuevo, dos espacios + Enter para salto de línea simple.</p>
<p class="text-xs text-gray-500 mt-1">Usa Markdown. Doble Enter para párrafo nuevo, dos espacios +
Enter para salto de línea simple.</p>
</div>
<div class="flex items-center">
<input type="checkbox" id="edit-script-hidden" class="form-checkbox h-5 w-5 mr-2">
<label for="edit-script-hidden" class="text-sm font-bold">Ocultar script (no se podrá ejecutar desde la UI)</label>
<label for="edit-script-hidden" class="text-sm font-bold">Ocultar script (no se podrá ejecutar desde
la UI)</label>
</div>
</div>
<div class="modal-footer sticky bottom-0 bg-white border-t p-4 flex justify-end gap-4">
<button onclick="closeScriptEditorModal()" class="bg-gray-500 text-white px-4 py-2 rounded">Cancelar</button>
<button onclick="saveScriptDetails()" class="bg-blue-500 text-white px-4 py-2 rounded">Guardar Cambios</button>
<button onclick="closeScriptEditorModal()"
class="bg-gray-500 text-white px-4 py-2 rounded">Cancelar</button>
<button onclick="saveScriptDetails()" class="bg-blue-500 text-white px-4 py-2 rounded">Guardar
Cambios</button>
</div>
</div>
</div>
<!-- Corregir la ruta del script -->
<script src="https://unpkg.com/markdown-it@14.1.0/dist/markdown-it.min.js"></script> <!-- Librería Markdown-it (unpkg) -->
<!-- Group Editor Modal (NUEVO) -->
<div id="group-editor-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-screen overflow-y-auto">
<div class="p-6">
<h3 class="text-xl font-semibold mb-6">Gestionar Grupos de Scripts GUI</h3>
<!-- Lista de grupos existentes -->
<div class="mb-6">
<h4 class="font-medium mb-3">Grupos Existentes</h4>
<div id="existing-groups-list" class="space-y-2 max-h-40 overflow-y-auto border rounded p-2">
<!-- Lista dinámica -->
</div>
</div>
<!-- Formulario de edición -->
<form id="group-form" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium mb-1">ID del Grupo</label>
<input type="text" id="group-id" class="w-full p-2 border rounded" required>
</div>
<div>
<label class="block text-sm font-medium mb-1">Nombre</label>
<input type="text" id="group-name" class="w-full p-2 border rounded" required>
</div>
</div>
<div>
<label class="block text-sm font-medium mb-1">Descripción</label>
<textarea id="group-description" class="w-full p-2 border rounded h-20"></textarea>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium mb-1">Categoría</label>
<select id="group-category" class="w-full p-2 border rounded">
<option value="Herramientas">🔧 Herramientas</option>
<option value="Análisis">📊 Análisis</option>
<option value="Utilidades">⚙️ Utilidades</option>
<option value="Desarrollo">💻 Desarrollo</option>
<option value="Visualización">📈 Visualización</option>
<option value="Otros">📁 Otros</option>
</select>
</div>
<div>
<label class="block text-sm font-medium mb-1">Versión</label>
<input type="text" id="group-version" class="w-full p-2 border rounded" value="1.0">
</div>
</div>
<div>
<label class="block text-sm font-medium mb-1">Directorio</label>
<div class="flex gap-2">
<input type="text" id="group-directory" class="flex-1 p-2 border rounded" required>
<button type="button" onclick="browseGroupDirectory()"
class="bg-gray-500 text-white px-4 py-2 rounded">
Explorar
</button>
</div>
</div>
<div class="flex justify-end gap-3 pt-4">
<button type="button" onclick="closeGroupEditor()"
class="px-4 py-2 text-gray-600 hover:text-gray-800">
Cancelar
</button>
<button type="button" onclick="deleteGroup()"
class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600" id="delete-group-btn"
style="display: none;">
Eliminar
</button>
<button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
Guardar
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Script Arguments Modal (NUEVO) -->
<div id="script-args-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 z-50">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white rounded-lg shadow-xl max-w-md w-full">
<div class="p-6">
<h3 class="text-lg font-semibold mb-4">Argumentos del Script</h3>
<div id="script-info" class="mb-4 p-3 bg-gray-50 rounded">
<div class="font-medium" id="script-display-name"></div>
<div class="text-sm text-gray-600" id="script-args-description"></div>
</div>
<div class="space-y-3">
<div>
<label class="block text-sm font-medium mb-1">
Argumentos de Línea de Comandos
</label>
<textarea id="script-args-input" class="w-full p-2 border rounded h-20"
placeholder="--input data.xlsx --output results.csv"></textarea>
<p class="text-xs text-gray-500 mt-1">
Separar argumentos con espacios. Usar comillas para valores con espacios.
</p>
</div>
</div>
<div class="flex justify-end gap-3 mt-6">
<button onclick="closeArgsModal()" class="px-4 py-2 text-gray-600 hover:text-gray-800">
Cancelar
</button>
<button onclick="executeWithArgs()"
class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
Ejecutar Script
</button>
</div>
</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>
window.addEventListener('load', () => {
console.log('Window loaded, initializing app...');
@ -276,4 +542,5 @@
});
</script>
</body>
</html>