Compare commits

...

4 Commits

30 changed files with 6191 additions and 751 deletions

174
.gitignore vendored Normal file
View File

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

110
app.py
View File

@ -1,9 +1,18 @@
from flask import Flask, render_template, request, jsonify, url_for from flask import Flask, render_template, request, jsonify, url_for
from flask_sock import Sock from flask_sock import Sock
from config_manager import ConfigurationManager from config_manager import ConfigurationManager
from datetime import datetime
import os import os
import json # Added import import json # Added import
from datetime import datetime
import time # Added for shutdown delay
# --- Imports for System Tray Icon ---
import threading
import webbrowser
import sys
import requests # To send shutdown request
from PIL import Image
import pystray
app = Flask( app = Flask(
__name__, static_url_path="", static_folder="static", template_folder="templates" __name__, static_url_path="", static_folder="static", template_folder="templates"
@ -14,6 +23,9 @@ config_manager = ConfigurationManager()
# Lista global para mantener las conexiones WebSocket activas # Lista global para mantener las conexiones WebSocket activas
websocket_connections = set() websocket_connections = set()
# --- Globals for Tray Icon ---
tray_icon = None
@sock.route("/ws") @sock.route("/ws")
def handle_websocket(ws): def handle_websocket(ws):
@ -227,5 +239,99 @@ def get_directory_history(group):
return jsonify(history) return jsonify(history)
# --- System Tray Icon Functions ---
def run_flask():
"""Runs the Flask app."""
print("Starting Flask server on http://127.0.0.1:5000/")
try:
# use_reloader=False is important when running in a thread
# For production, consider using waitress or gunicorn instead of app.run
app.run(host='127.0.0.1', port=5000, debug=True, use_reloader=False)
except Exception as e:
print(f"Error running Flask app: {e}")
# Optionally try to stop the tray icon if Flask fails critically
if tray_icon:
print("Attempting to stop tray icon due to Flask error.")
tray_icon.stop()
def open_app_browser(icon, item):
"""Callback function to open the browser."""
print("Opening application in browser...")
webbrowser.open("http://127.0.0.1:5000/")
def shutdown_flask_server():
"""Attempts to gracefully shut down the Werkzeug server."""
try:
# This requires the development server (werkzeug)
# Send a request to a special shutdown route
requests.post("http://127.0.0.1:5000/_shutdown", timeout=1)
except Exception as e:
print(f"Could not send shutdown request to Flask server: {e}")
print("Flask server might need to be closed manually.")
def stop_icon_thread():
"""Helper function to stop the icon after a delay, allowing HTTP response."""
time.sleep(0.1) # Small delay to allow the HTTP response to be sent
if tray_icon:
print("Stopping tray icon from shutdown route...")
tray_icon.stop()
else:
print("Tray icon not available to stop.")
# As a last resort if the icon isn't running for some reason
# print("Attempting os._exit(0) as fallback.")
# os._exit(0) # Force exit - use with caution
@app.route('/_shutdown', methods=['POST'])
def shutdown_route():
"""Internal route to shut down the application via the tray icon."""
print("Shutdown endpoint called.")
# Stop the main application thread by stopping the tray icon.
# Do this in a separate thread to allow the HTTP response to return first.
stopper = threading.Thread(target=stop_icon_thread, daemon=True)
stopper.start()
print("Shutdown signal sent to tray icon thread.")
return jsonify(status="success", message="Application shutdown initiated..."), 200
def exit_application(icon, item):
"""Callback function to exit the application."""
print("Exit requested via tray menu.")
# Just stop the icon. This will end the main thread, and the daemon Flask thread will exit.
print("Stopping tray icon...")
if icon: # pystray passes the icon object
icon.stop()
elif tray_icon: # Fallback just in case
tray_icon.stop()
if __name__ == "__main__": if __name__ == "__main__":
app.run(debug=True) # --- Start Flask in a background thread ---
flask_thread = threading.Thread(target=run_flask, daemon=True)
flask_thread.start()
# --- Setup and run the system tray icon ---
icon_path = r"d:\Proyectos\Scripts\ParamManagerScripts\icon.png" # Use absolute path
try:
image = Image.open(icon_path)
menu = pystray.Menu(
pystray.MenuItem("Abrir ParamManager", open_app_browser, default=True),
pystray.MenuItem("Salir", exit_application)
)
tray_icon = pystray.Icon("ParamManager", image, "ParamManager", menu)
print("Starting system tray icon...")
tray_icon.run() # This blocks the main thread until icon.stop() is called
except FileNotFoundError:
print(f"Error: Icono no encontrado en '{icon_path}'. El icono de notificación no se iniciará.", file=sys.stderr)
print("La aplicación Flask seguirá ejecutándose en segundo plano. Presiona Ctrl+C para detenerla si es necesario.")
# Keep the main thread alive so the Flask thread doesn't exit immediately
# This allows Flask to continue running even without the tray icon.
try:
while flask_thread.is_alive():
flask_thread.join(timeout=1.0) # Wait indefinitely
except KeyboardInterrupt:
print("\nCtrl+C detectado. Intentando detener Flask...")
shutdown_flask_server() # Try to shutdown Flask on Ctrl+C too
print("Saliendo.")
except Exception as e:
print(f"Error al iniciar el icono de notificación: {e}", file=sys.stderr)
print("Aplicación finalizada.")

View File

@ -0,0 +1,34 @@
--- Log de Ejecución: x1.py ---
Grupo: EmailCrono
Directorio de Trabajo: C:\Trabajo\SIDEL\EMAILs\I_ E5.007727 _ Evo On - SFSRFH300172 + SFSRFH300109 - ANDIA LACTEOS
Inicio: 2025-05-03 17:15:12
Fin: 2025-05-03 17:15:14
Duración: 0:00:01.628641
Estado: SUCCESS (Código de Salida: 0)
--- SALIDA ESTÁNDAR (STDOUT) ---
Working directory: C:\Trabajo\SIDEL\EMAILs\I_ E5.007727 _ Evo On - SFSRFH300172 + SFSRFH300109 - ANDIA LACTEOS
Input directory: C:\Trabajo\SIDEL\EMAILs\I_ E5.007727 _ Evo On - SFSRFH300172 + SFSRFH300109 - ANDIA LACTEOS
Output directory: C:/Users/migue/OneDrive/Miguel/Obsidean/Trabajo/VM/04-SIDEL/00 - MASTER/EMAILs
Cronologia file: C:/Users/migue/OneDrive/Miguel/Obsidean/Trabajo/VM/04-SIDEL/00 - MASTER/EMAILs\cronologia.md
Attachments directory: C:\Trabajo\SIDEL\EMAILs\I_ E5.007727 _ Evo On - SFSRFH300172 + SFSRFH300109 - ANDIA LACTEOS\adjuntos
Beautify rules file: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\EmailCrono\config\beautify_rules.json
Found 1 .eml files
Loaded 0 existing messages
Processing C:\Trabajo\SIDEL\EMAILs\I_ E5.007727 _ Evo On - SFSRFH300172 + SFSRFH300109 - ANDIA LACTEOS\I_ E5.007727 _ Evo On - SFSRFH300172 + SFSRFH300109 - ANDIA LACTEOS.eml
Aplicando reglas de prioridad 1
Aplicando reglas de prioridad 2
Aplicando reglas de prioridad 3
Aplicando reglas de prioridad 4
Estadísticas de procesamiento:
- Total mensajes encontrados: 1
- Mensajes únicos añadidos: 1
- Mensajes duplicados ignorados: 0
Writing 1 messages to C:/Users/migue/OneDrive/Miguel/Obsidean/Trabajo/VM/04-SIDEL/00 - MASTER/EMAILs\cronologia.md
--- ERRORES (STDERR) ---
Ninguno
--- FIN DEL LOG ---

View File

@ -0,0 +1,14 @@
{
"level1": {
"api_key": "your-api-key-here",
"model": "gpt-3.5-turbo"
},
"level2": {
"attachments_dir": "adjuntos",
"cronologia_file": "cronologia.md"
},
"level3": {
"output_directory": "C:/Users/migue/OneDrive/Miguel/Obsidean/Trabajo/VM/04-SIDEL/00 - MASTER/EMAILs"
},
"working_directory": "C:\\Trabajo\\SIDEL\\EMAILs\\I_ E5.007727 _ Evo On - SFSRFH300172 + SFSRFH300109 - ANDIA LACTEOS"
}

View File

@ -1,6 +1,8 @@
{ {
"path": "C:\\Trabajo\\VM\\40 - 93040 - HENKEL - NEXT2 Problem\\Reporte\\EmailTody", "path": "C:\\Trabajo\\SIDEL\\EMAILs\\I_ E5.007727 _ Evo On - SFSRFH300172 + SFSRFH300109 - ANDIA LACTEOS",
"history": [ "history": [
"C:\\Trabajo\\SIDEL\\EMAILs\\I_ E5.007727 _ Evo On - SFSRFH300172 + SFSRFH300109 - ANDIA LACTEOS",
"C:\\Estudio",
"C:\\Trabajo\\VM\\40 - 93040 - HENKEL - NEXT2 Problem\\Reporte\\EmailTody", "C:\\Trabajo\\VM\\40 - 93040 - HENKEL - NEXT2 Problem\\Reporte\\EmailTody",
"C:\\Trabajo\\VM\\30 - 9.3941- Kosme - Portogallo (Modifica + Linea)\\Reporte\\Emails", "C:\\Trabajo\\VM\\30 - 9.3941- Kosme - Portogallo (Modifica + Linea)\\Reporte\\Emails",
"C:\\Users\\migue\\OneDrive\\Miguel\\Obsidean\\Trabajo\\VM\\30 - 9.3941- Kosme - Portogallo (Modifica + Linea)\\Emails", "C:\\Users\\migue\\OneDrive\\Miguel\\Obsidean\\Trabajo\\VM\\30 - 9.3941- Kosme - Portogallo (Modifica + Linea)\\Emails",

View File

@ -0,0 +1,6 @@
{
"name": "Exportador de objetos de Tia Portal y procesador de CAx",
"description": "Este conjunto de scripts exporta desde Tia Portal los objetos en fomarto XML y los objetos CAx. Luego se puede generar documentacion desde estos CAx de la periferia IO del PLC exportado.",
"version": "1.0",
"author": "Miguel"
}

View File

@ -0,0 +1,11 @@
{
"scl_output_dir": "scl_output",
"xref_output_dir": "xref_output",
"xref_source_subdir": "source",
"call_xref_filename": "xref_calls_tree.md",
"db_usage_xref_filename": "xref_db_usage_summary.md",
"plc_tag_xref_filename": "xref_plc_tags_summary.md",
"max_call_depth": 5,
"max_users_list": 20,
"aggregated_filename": "full_project_representation.md"
}

View File

@ -0,0 +1,6 @@
{
"name": "Procesador de XML exportado de TIA",
"description": "Conjunto de scripts que procesan archivos XML exportados de TIA, conviertiendo los objetos LAD a SCL y generando documentación en formato Markdown. ",
"version": "1.0",
"author": "Miguel"
}

View File

@ -1,4 +1,59 @@
{ {
"type": "object", "type": "object",
"properties": {} "properties": {
"scl_output_dir": {
"type": "string",
"title": "Directorio Salida SCL/MD (x3)",
"description": "Nombre del directorio (relativo a la raíz del proyecto PLC) donde x3 genera archivos .scl/.md, y x4/x5 leen.",
"default": "scl_output"
},
"xref_output_dir": {
"type": "string",
"title": "Directorio Salida XRef (x4)",
"description": "Nombre del directorio (relativo a la raíz del proyecto PLC) donde x4 genera archivos de referencias cruzadas.",
"default": "xref_output"
},
"xref_source_subdir": {
"type": "string",
"title": "Subdirectorio Fuentes XRef (x4)",
"description": "Nombre del subdirectorio dentro de xref_output_dir donde x4 coloca archivos fuente (.md) preparados para enlaces Obsidian.",
"default": "source"
},
"call_xref_filename": {
"type": "string",
"title": "Nombre Archivo Árbol Llamadas (x4)",
"description": "Nombre del archivo para la salida del árbol de llamadas generado por x4.",
"default": "xref_calls_tree.md"
},
"db_usage_xref_filename": {
"type": "string",
"title": "Nombre Archivo Uso DBs (x4)",
"description": "Nombre del archivo para el resumen de uso de DBs generado por x4.",
"default": "xref_db_usage_summary.md"
},
"plc_tag_xref_filename": {
"type": "string",
"title": "Nombre Archivo Uso PLC Tags (x4)",
"description": "Nombre del archivo para el resumen de uso de PLC Tags generado por x4.",
"default": "xref_plc_tags_summary.md"
},
"max_call_depth": {
"type": "integer",
"title": "Profundidad Máx. Árbol Llamadas (x4)",
"description": "Profundidad máxima de recursión para el árbol de llamadas generado por x4.",
"default": 5
},
"max_users_list": {
"type": "integer",
"title": "Máx. Usuarios Listados (x4)",
"description": "Número máximo de usuarios listados por DB/Tag en los resúmenes generados por x4.",
"default": 20
},
"aggregated_filename": {
"type": "string",
"title": "Nombre Archivo Agregado (x5)",
"description": "Nombre del archivo Markdown agregado final generado por x5 (se guarda en el directorio de trabajo principal).",
"default": "full_project_representation.md"
}
}
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,19 @@
{
"level1": {
"api_key": "your-api-key-here",
"model": "gpt-3.5-turbo"
},
"level2": {
"scl_output_dir": "scl_output",
"xref_output_dir": "xref_output",
"xref_source_subdir": "source",
"call_xref_filename": "xref_calls_tree.md",
"db_usage_xref_filename": "xref_db_usage_summary.md",
"plc_tag_xref_filename": "xref_plc_tags_summary.md",
"max_call_depth": 5,
"max_users_list": 20,
"aggregated_filename": "full_project_representation.md"
},
"level3": {},
"working_directory": "C:\\Trabajo\\SIDEL\\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\\Reporte\\IOExport"
}

View File

@ -0,0 +1,6 @@
{
"path": "C:\\Trabajo\\SIDEL\\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\\Reporte\\IOExport",
"history": [
"C:\\Trabajo\\SIDEL\\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\\Reporte\\IOExport"
]
}

View File

@ -23,15 +23,16 @@ script_root = os.path.dirname(
sys.path.append(script_root) sys.path.append(script_root)
from backend.script_utils import load_configuration from backend.script_utils import load_configuration
# --- Funciones (get_console_encoding - sin cambios) --- # <-- NUEVO: Importar funciones directamente -->
def get_console_encoding(): from x1_to_json import convert_xml_to_json
try: from x2_process import process_json_to_scl
return locale.getpreferredencoding(False) from x3_generate_scl import generate_scl_or_markdown
except Exception: # <-- NUEVO: Importar funciones de x4 y x5 -->
return "cp1252" from x4_cross_reference import generate_cross_references # Asumiendo que x4_cross_reference.py tiene esta función
from x5_aggregate import aggregate_outputs
CONSOLE_ENCODING = get_console_encoding() CONSOLE_ENCODING = "utf-8"
# <-- NUEVO: Importar format_variable_name (necesario para predecir nombre de salida) --> # <-- NUEVO: Importar format_variable_name (necesario para predecir nombre de salida) -->
try: try:
@ -85,117 +86,7 @@ def log_message(message, log_file_handle, also_print=True):
# <-- FIN NUEVO --> # <-- FIN NUEVO -->
# <-- MODIFICADO: run_script para aceptar log_file_handle --> # <-- run_script ya no es necesaria -->
def run_script(script_name, xml_arg, log_file_handle, *extra_args):
"""Runs a given script, logs output, and returns success status."""
script_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), script_name)
python_executable = sys.executable
command = [python_executable, script_path, os.path.abspath(xml_arg)]
command.extend(extra_args)
# Loguear el comando que se va a ejecutar
log_message(
f"--- Running {script_name} with arguments: {[os.path.relpath(arg) if isinstance(arg, str) and os.path.exists(arg) else arg for arg in command[2:]]} ---",
log_file_handle,
)
try:
result = subprocess.run(
command,
check=True,
capture_output=True,
text=True,
encoding=CONSOLE_ENCODING,
errors="replace",
)
stdout_clean = result.stdout.strip() if result.stdout else ""
stderr_clean = result.stderr.strip() if result.stderr else ""
# Loguear stdout si existe
if stdout_clean:
log_message(
f"--- Stdout ({script_name}) ---", log_file_handle, also_print=False
) # Loguear encabezado
log_message(
stdout_clean, log_file_handle, also_print=True
) # Loguear y mostrar contenido
log_message(
f"--- End Stdout ({script_name}) ---", log_file_handle, also_print=False
) # Loguear fin
# Loguear stderr si existe
if stderr_clean:
# Usar log_message también para stderr, pero imprimir en consola como error
log_message(
f"--- Stderr ({script_name}) ---", log_file_handle, also_print=False
) # Loguear encabezado
log_message(
stderr_clean, log_file_handle, also_print=False
) # Loguear contenido
log_message(
f"--- End Stderr ({script_name}) ---", log_file_handle, also_print=False
) # Loguear fin
# Imprimir stderr en la consola de error estándar
print(f"--- Stderr ({script_name}) ---", file=sys.stderr)
print(stderr_clean, file=sys.stderr)
print("--------------------------", file=sys.stderr)
return True # Éxito
except FileNotFoundError:
error_msg = f"Error: Script '{script_path}' or Python executable '{python_executable}' not found."
log_message(error_msg, log_file_handle, also_print=False) # Loguear error
print(error_msg, file=sys.stderr) # Mostrar error en consola
return False
except subprocess.CalledProcessError as e:
error_msg = f"Error running {script_name}: Script returned non-zero exit code {e.returncode}."
log_message(error_msg, log_file_handle, also_print=False) # Loguear error
print(error_msg, file=sys.stderr) # Mostrar error en consola
stdout_decoded = e.stdout.strip() if e.stdout else ""
stderr_decoded = e.stderr.strip() if e.stderr else ""
if stdout_decoded:
log_message(
f"--- Stdout ({script_name} - Error) ---",
log_file_handle,
also_print=False,
)
log_message(stdout_decoded, log_file_handle, also_print=False)
log_message(
f"--- End Stdout ({script_name} - Error) ---",
log_file_handle,
also_print=False,
)
print(f"--- Stdout ({script_name}) ---", file=sys.stderr)
print(stdout_decoded, file=sys.stderr)
if stderr_decoded:
log_message(
f"--- Stderr ({script_name} - Error) ---",
log_file_handle,
also_print=False,
)
log_message(stderr_decoded, log_file_handle, also_print=False)
log_message(
f"--- End Stderr ({script_name} - Error) ---",
log_file_handle,
also_print=False,
)
print(f"--- Stderr ({script_name}) ---", file=sys.stderr)
print(stderr_decoded, file=sys.stderr)
print("--------------------------", file=sys.stderr)
return False
except Exception as e:
error_msg = f"An unexpected error occurred while running {script_name}: {e}"
log_message(error_msg, log_file_handle, also_print=False) # Loguear error
traceback_str = traceback.format_exc()
log_message(
traceback_str, log_file_handle, also_print=False
) # Loguear traceback
print(error_msg, file=sys.stderr) # Mostrar error en consola
traceback.print_exc(file=sys.stderr) # Mostrar traceback en consola
return False
# --- Función check_skip_status (sin cambios en su lógica interna) --- # --- Función check_skip_status (sin cambios en su lógica interna) ---
@ -266,16 +157,29 @@ def check_skip_status(
return status return status
# --- Constantes ---
AGGREGATED_FILENAME = "full_project_representation.md"
SCL_OUTPUT_DIRNAME = "scl_output"
XREF_OUTPUT_DIRNAME = "xref_output"
# --- Bloque Principal --- # --- Bloque Principal ---
if __name__ == "__main__": if __name__ == "__main__":
configs = load_configuration() configs = load_configuration()
working_directory = configs.get("working_directory") working_directory = configs.get("working_directory")
group_config = configs.get("level2", {})
# <-- NUEVO: Leer parámetros de configuración para x3, x4, x5 -->
xml_parser_config = configs.get("XML Parser to SCL", {})
cfg_scl_output_dirname = xml_parser_config.get("scl_output_dir", "scl_output")
cfg_xref_output_dirname = xml_parser_config.get("xref_output_dir", "xref_output")
cfg_xref_source_subdir = xml_parser_config.get("xref_source_subdir", "source")
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)
cfg_aggregated_filename = xml_parser_config.get("aggregated_filename", "full_project_representation.md")
# <-- FIN NUEVO -->
# Directorio donde se encuentra este script (x0_main.py)
script_dir = os.path.dirname(os.path.abspath(__file__))
# <-- MODIFICADO: Abrir archivo log --> # <-- MODIFICADO: Abrir archivo log -->
log_filepath = os.path.join( log_filepath = os.path.join(
@ -287,19 +191,28 @@ if __name__ == "__main__":
log_message("=" * 40 + " LOG START " + "=" * 40, log_f) log_message("=" * 40 + " LOG START " + "=" * 40, log_f)
# --- PARTE 1: BUSCAR ARCHIVOS --- # --- PARTE 1: BUSCAR ARCHIVOS ---
xml_project_dir = working_directory # <-- MODIFICADO: Apuntar al subdirectorio 'PLC' dentro del working_directory -->
plc_subdir_name = "PLC" # Nombre estándar del subdirectorio de TIA Portal
xml_project_dir = os.path.join(working_directory, plc_subdir_name)
log_message( log_message(
f"Buscando archivos XML recursivamente en: '{xml_project_dir}'", log_f f"Directorio de trabajo base configurado: '{working_directory}'", log_f
) )
log_message(
f"Buscando archivos XML recursivamente en el subdirectorio: '{xml_project_dir}'", log_f
)
# Verificar si el directorio PLC existe
if not os.path.isdir(xml_project_dir): if not os.path.isdir(xml_project_dir):
log_message( log_message(
f"Error: El directorio '{xml_project_dir}' no existe.", f"Error: El subdirectorio '{plc_subdir_name}' no existe dentro de '{working_directory}'. "
f"Se esperaba encontrar la estructura del proyecto TIA Portal en '{xml_project_dir}'.",
log_f, log_f,
also_print=False, also_print=False,
) )
print( print(
f"Error: El directorio '{xml_project_dir}' no existe.", file=sys.stderr f"Error: El subdirectorio '{plc_subdir_name}' no existe dentro de '{working_directory}'. "
f"Asegúrese de que la ruta del directorio de trabajo apunte a la carpeta que *contiene* la carpeta '{plc_subdir_name}'.", file=sys.stderr
) )
sys.exit(1) sys.exit(1)
search_pattern = os.path.join(xml_project_dir, "**", "*.xml") search_pattern = os.path.join(xml_project_dir, "**", "*.xml")
@ -315,35 +228,37 @@ if __name__ == "__main__":
) )
xml_files_found.sort() xml_files_found.sort()
[ [
log_message(f" - {os.path.relpath(xml_file, script_dir)}", log_f) log_message(f" - {os.path.relpath(xml_file, working_directory)}", log_f) # Mostrar ruta relativa al working_directory original
for xml_file in xml_files_found for xml_file in xml_files_found
] ]
# --- Directorios de salida --- # --- Directorios de salida ---
scl_output_dir = os.path.join(xml_project_dir, SCL_OUTPUT_DIRNAME) # Estos directorios ahora se crearán DENTRO de xml_project_dir (es decir, dentro de 'PLC')
xref_output_dir = os.path.join(xml_project_dir, XREF_OUTPUT_DIRNAME) scl_output_dir = os.path.join(xml_project_dir, cfg_scl_output_dirname) # Usar valor de config
xref_output_dir = os.path.join(xml_project_dir, cfg_xref_output_dirname) # Usar valor de config
# --- PARTE 2: PROCESAMIENTO INDIVIDUAL (x1, x2, x3) --- # --- PARTE 2: PROCESAMIENTO INDIVIDUAL (x1, x2, x3) ---
log_message("\n--- Fase 1: Procesamiento Individual (x1, x2, x3) ---", log_f) log_message("\n--- Fase 1: Procesamiento Individual (x1, x2, x3) ---", log_f)
script1 = "x1_to_json.py" # Los nombres de script ya no se usan directamente para x1, x2, x3
script2 = "x2_process.py" # script1 = "x1_to_json.py"
script3 = "x3_generate_scl.py" # script2 = "x2_process.py"
file_status = {} # script3 = "x3_generate_scl.py"
processed_count = 0 processed_count = 0
skipped_full_count = 0 skipped_full_count = 0
failed_count = 0 failed_count = 0
skipped_partial_count = 0 skipped_partial_count = 0
for xml_filepath in xml_files_found: for i, xml_filepath in enumerate(xml_files_found):
relative_path = os.path.relpath(xml_filepath, script_dir) relative_path = os.path.relpath(xml_filepath, working_directory)
log_message(f"\n--- Procesando archivo: {relative_path} ---", log_f) log_message(f"\n--- Procesando archivo: {relative_path} ---", log_f)
status = {"x1_ok": None, "x2_ok": None, "x3_ok": None}
file_status[relative_path] = status
base_filename = os.path.splitext(os.path.basename(xml_filepath))[0] base_filename = os.path.splitext(os.path.basename(xml_filepath))[0]
parsing_dir = os.path.join(os.path.dirname(xml_filepath), "parsing") parsing_dir = os.path.join(os.path.dirname(xml_filepath), "parsing")
# Crear directorio de parsing si no existe
os.makedirs(parsing_dir, exist_ok=True)
json_output_file = os.path.join(parsing_dir, f"{base_filename}.json")
processed_json_filepath = os.path.join( processed_json_filepath = os.path.join(
parsing_dir, f"{base_filename}_processed.json" parsing_dir, f"{base_filename}_processed.json" # <-- Corregido: nombre correcto
) )
# 1. Comprobar estado de salto # 1. Comprobar estado de salto
@ -353,139 +268,184 @@ if __name__ == "__main__":
skip_x1_x2 = skip_info["skip_x1_x2"] skip_x1_x2 = skip_info["skip_x1_x2"]
skip_x3 = skip_info["skip_x3"] skip_x3 = skip_info["skip_x3"]
# 2. Ejecutar/Saltar x1 # Si se salta todo, registrar y continuar
if skip_x1_x2: if skip_x1_x2 and skip_x3:
log_message( log_message(
f"--- SALTANDO x1 para: {relative_path} (archivo XML no modificado y JSON procesado existe)", f"--- SALTANDO TODO (x1, x2, x3) para: {relative_path} (XML no modificado, salida final actualizada)",
log_f, log_f,
) )
status["x1_ok"] = True
else:
if run_script(script1, xml_filepath, log_f): # Pasar log_f
# Mensaje ya logueado por run_script
status["x1_ok"] = True
else:
log_message(
f"--- {script1} FALLÓ para: {relative_path} ---",
log_f,
also_print=False,
) # Ya impreso por run_script
status["x1_ok"] = False
failed_count += 1
continue
# 3. Ejecutar/Saltar x2
if skip_x1_x2:
log_message(
f"--- SALTANDO x2 para: {relative_path} (razón anterior)", log_f
)
status["x2_ok"] = True
else:
if run_script(script2, xml_filepath, log_f): # Pasar log_f
status["x2_ok"] = True
else:
log_message(
f"--- {script2} FALLÓ para: {relative_path} ---",
log_f,
also_print=False,
)
status["x2_ok"] = False
failed_count += 1
continue
# 4. Ejecutar/Saltar x3
if skip_x3: # Solo puede ser True si skip_x1_x2 era True
log_message(
f"--- SALTANDO x3 para: {relative_path} (archivo de salida en '{SCL_OUTPUT_DIRNAME}' está actualizado)",
log_f,
)
status["x3_ok"] = True
skipped_full_count += 1 skipped_full_count += 1
processed_count += 1 processed_count += 1 # Contar como procesado si se salta todo
else: continue
# Usar try/except para capturar errores en las llamadas directas
try:
# 2. Ejecutar/Saltar x1 (convert_xml_to_json)
if skip_x1_x2: if skip_x1_x2:
skipped_partial_count += 1 # Se saltó x1/x2 pero se ejecuta x3 log_message(
if run_script( f"--- SALTANDO x1 para: {relative_path} (XML no modificado, JSON procesado existe)",
script3, xml_filepath, log_f, xml_project_dir log_f,
): # Pasar log_f y project_root_dir )
status["x3_ok"] = True success_x1 = True # Asumir éxito si se salta
processed_count += 1
else: else:
log_message( log_message(
f"--- {script3} FALLÓ para: {relative_path} ---", f"--- Ejecutando x1 (convert_xml_to_json) para: {relative_path} ---", log_f
log_f,
also_print=False,
) )
status["x3_ok"] = False success_x1 = convert_xml_to_json(xml_filepath, json_output_file)
if not success_x1:
log_message(f"--- x1 FALLÓ para: {relative_path} ---", log_f, also_print=False) # La función ya imprime el error
if not success_x1:
failed_count += 1 failed_count += 1
continue continue # No continuar si x1 falló
# 3. Ejecutar/Saltar x2 (process_json_to_scl)
if skip_x1_x2: # Si se saltó x1, también se salta x2
log_message(
f"--- SALTANDO x2 para: {relative_path} (razón anterior)", log_f
)
success_x2 = True # Asumir éxito si se salta
else:
log_message(
f"--- Ejecutando x2 (process_json_to_scl) para: {relative_path} ---", log_f
)
success_x2 = process_json_to_scl(json_output_file, processed_json_filepath)
if not success_x2:
log_message(f"--- x2 FALLÓ para: {relative_path} ---", log_f, also_print=False)
if not success_x2:
failed_count += 1
continue # No continuar si x2 falló
# 4. Ejecutar x3 (generate_scl_or_markdown) - skip_x3 ya se manejó al principio
# Si llegamos aquí, x3 SIEMPRE debe ejecutarse (porque skip_x3 era False)
if skip_x1_x2:
skipped_partial_count += 1 # Se saltó x1/x2 pero se ejecuta x3
log_message(
f"--- Ejecutando x3 (generate_scl_or_markdown) para: {relative_path} ---", log_f
)
# Asegurar que el directorio de salida final exista ANTES de llamar a la función
os.makedirs(scl_output_dir, exist_ok=True)
success_x3 = generate_scl_or_markdown(
processed_json_filepath, scl_output_dir, xml_project_dir
)
if not success_x3:
log_message(f"--- x3 FALLÓ para: {relative_path} ---", log_f, also_print=False)
failed_count += 1
continue # No continuar si x3 falló
# Si todo fue bien
processed_count += 1
except Exception as e:
# Capturar cualquier error inesperado durante las llamadas a funciones
log_message(f"--- ERROR INESPERADO procesando {relative_path}: {e} ---", log_f, also_print=False)
print(f"--- ERROR INESPERADO procesando {relative_path}: {e} ---", file=sys.stderr)
traceback_str = traceback.format_exc()
log_message(traceback_str, log_f, also_print=False) # Loguear traceback
traceback.print_exc(file=sys.stderr) # Mostrar traceback en consola
failed_count += 1
continue # Pasar al siguiente archivo
# --- PARTE 3: EJECUTAR x4 (Referencias Cruzadas) --- # --- PARTE 3: EJECUTAR x4 (Referencias Cruzadas) ---
log_message( log_message(
f"\n--- Fase 2: Ejecutando x4_cross_reference.py (salida en '{XREF_OUTPUT_DIRNAME}/') ---", f"\n--- Fase 2: Ejecutando x4_cross_reference.py (salida en '{cfg_xref_output_dirname}/') ---", # Usar valor de config
log_f, log_f,
) )
script4 = "x4_cross_reference.py"
run_x4 = True run_x4 = True
success_x4 = False success_x4 = False
can_run_x4 = any(s["x1_ok"] and s["x2_ok"] for s in file_status.values()) # La condición para ejecutar x4 ahora depende de si *algún* archivo tuvo éxito en x1 y x2
if not can_run_x4: # (Necesitamos una forma de rastrear esto, o simplemente intentarlo si no hubo fallos fatales antes)
log_message( # Simplificación: Ejecutar x4 si no todos los archivos fallaron en x1/x2.
"Advertencia: Ningún archivo completó x1/x2. Saltando x4.", log_f # Una mejor comprobación sería ver si existe algún archivo _processed.json
can_run_x4 = failed_count < len(xml_files_found) # Aproximación simple
if not can_run_x4 and len(xml_files_found) > 0:
log_message(
"Advertencia: Todos los archivos fallaron en x1/x2. Saltando x4.", log_f
) )
run_x4 = False run_x4 = False
script4_path = os.path.join(script_dir, script4) elif len(xml_files_found) == 0:
if not os.path.exists(script4_path): run_x4 = False # No hay archivos, no ejecutar
log_message(
f"Advertencia: Script '{script4}' no encontrado. Saltando x4.", log_f
)
run_x4 = False
if run_x4: if run_x4:
log_message( log_message(
f"Ejecutando {script4} sobre: {xml_project_dir}, salida en: {xref_output_dir}", f"Ejecutando x4 (generate_cross_references) sobre: {xml_project_dir}, salida en: {xref_output_dir}",
log_f, log_f,
) )
success_x4 = run_script( try:
script4, xml_project_dir, log_f, "-o", xref_output_dir # Llamada directa a la función de x4
) # Pasar log_f # <-- MODIFICADO: Pasar todos los parámetros leídos de la config -->
if not success_x4: success_x4 = generate_cross_references(
log_message(f"--- {script4} FALLÓ. ---", log_f, also_print=False) xml_project_dir,
# Mensaje de éxito ya logueado por run_script xref_output_dir,
cfg_scl_output_dirname,
cfg_xref_source_subdir,
cfg_call_xref_filename,
cfg_db_usage_xref_filename,
cfg_plc_tag_xref_filename,
cfg_max_call_depth,
cfg_max_users_list)
if not success_x4:
# La función interna ya debería haber impreso/logueado el error específico
log_message(f"--- x4 (generate_cross_references) FALLÓ. ---", log_f, also_print=False)
except Exception as e:
# Capturar error inesperado en la llamada a x4
log_message(f"--- ERROR INESPERADO ejecutando x4: {e} ---", log_f, also_print=False)
print(f"--- ERROR INESPERADO ejecutando x4: {e} ---", file=sys.stderr)
traceback_str = traceback.format_exc()
log_message(traceback_str, log_f, also_print=False)
traceback.print_exc(file=sys.stderr)
success_x4 = False # Marcar como fallo
else: else:
log_message("Fase 2 (x4) omitida.", log_f) log_message("Fase 2 (x4) omitida.", log_f)
# --- PARTE 4: EJECUTAR x5 (Agregación) --- # --- PARTE 4: EJECUTAR x5 (Agregación) ---
log_message(f"\n--- Fase 3: Ejecutando x5_aggregate.py ---", log_f) log_message(
script5 = "x5_aggregate.py" f"\n--- Fase 3: Ejecutando x5_aggregate.py (salida en '{cfg_aggregated_filename}') ---", # Usar valor de config
log_f
)
run_x5 = True run_x5 = True
success_x5 = False success_x5 = False
can_run_x5 = any(s["x3_ok"] for s in file_status.values()) # Condición similar a x4: ejecutar si no todo falló en x1/x2/x3
if not can_run_x5: can_run_x5 = failed_count < len(xml_files_found)
log_message("Advertencia: Ningún archivo completó x3. Saltando x5.", log_f) if not can_run_x5 and len(xml_files_found) > 0:
run_x5 = False
script5_path = os.path.join(script_dir, script5)
if not os.path.exists(script5_path):
log_message( log_message(
f"Advertencia: Script '{script5}' no encontrado. Saltando x5.", log_f "Advertencia: Todos los archivos fallaron en x1/x2/x3. Saltando x5.", log_f
) )
run_x5 = False run_x5 = False
elif len(xml_files_found) == 0:
run_x5 = False
if run_x5: if run_x5:
output_agg_file = os.path.join(xml_project_dir, AGGREGATED_FILENAME) output_agg_file = os.path.join(working_directory, cfg_aggregated_filename) # Usar valor de config
log_message( log_message(
f"Ejecutando {script5} sobre: {xml_project_dir}, salida en: {output_agg_file}", f"Ejecutando x5 (aggregate_outputs) sobre: {xml_project_dir}, salida agregada en: {output_agg_file}",
log_f, log_f
) )
success_x5 = run_script( try:
script5, xml_project_dir, log_f, "-o", output_agg_file # Llamada directa a la función de x5
) # Pasar log_f # <-- MODIFICADO: Pasar los parámetros necesarios leídos de la config -->
if not success_x5: success_x5 = aggregate_outputs(
log_message(f"--- {script5} FALLÓ. ---", log_f, also_print=False) xml_project_dir,
# Mensaje de éxito ya logueado por run_script output_agg_file,
cfg_scl_output_dirname,
cfg_xref_output_dirname)
if not success_x5:
# La función interna ya debería haber impreso/logueado el error específico
log_message(f"--- x5 (aggregate_outputs) FALLÓ. ---", log_f, also_print=False)
except Exception as e:
# Capturar error inesperado en la llamada a x5
log_message(f"--- ERROR INESPERADO ejecutando x5: {e} ---", log_f, also_print=False)
print(f"--- ERROR INESPERADO ejecutando x5: {e} ---", file=sys.stderr)
traceback_str = traceback.format_exc()
log_message(traceback_str, log_f, also_print=False)
traceback.print_exc(file=sys.stderr)
success_x5 = False # Marcar como fallo
else: else:
log_message("Fase 3 (x5) omitida.", log_f) log_message("Fase 3 (x5) omitida.", log_f)
# --- PARTE 5: RESUMEN FINAL --- (MOVIDO AQUÍ)
# --- PARTE 5: RESUMEN FINAL --- # --- PARTE 5: RESUMEN FINAL ---
log_message( log_message(
"\n" + "-" * 20 + " Resumen Final del Procesamiento Completo " + "-" * 20, "\n" + "-" * 20 + " Resumen Final del Procesamiento Completo " + "-" * 20,
@ -503,21 +463,13 @@ if __name__ == "__main__":
f"Archivos parcialmente saltados (x1, x2 saltados; x3 ejecutado): {skipped_partial_count}", f"Archivos parcialmente saltados (x1, x2 saltados; x3 ejecutado): {skipped_partial_count}",
log_f, log_f,
) )
log_message(f"Archivos fallidos (en x1, x2 o x3): {failed_count}", log_f) log_message(f"Archivos fallidos (en x1, x2, x3 o error inesperado): {failed_count}", log_f)
if failed_count > 0: # El detalle de archivos fallidos es más difícil de rastrear ahora sin el dict 'file_status'
log_message("Archivos fallidos:", log_f) # Se podría reintroducir si es necesario, actualizándolo en cada paso.
for f, s in file_status.items(): # Por ahora, solo mostramos el conteo.
if not ( # if failed_count > 0:
s.get("x1_ok", False) # log_message("Archivos fallidos:", log_f)
and s.get("x2_ok", False) # ... (lógica para mostrar cuáles fallaron) ...
and s.get("x3_ok", False)
):
failed_step = (
"x1"
if not s.get("x1_ok", False)
else ("x2" if not s.get("x2_ok", False) else "x3")
)
log_message(f" - {f} (falló en {failed_step})", log_f)
log_message( log_message(
f"Fase 2 (Generación XRef - x4): {'Completada' if run_x4 and success_x4 else ('Fallida' if run_x4 and not success_x4 else 'Omitida')}", f"Fase 2 (Generación XRef - x4): {'Completada' if run_x4 and success_x4 else ('Fallida' if run_x4 and not success_x4 else 'Omitida')}",
log_f, log_f,
@ -555,5 +507,5 @@ if __name__ == "__main__":
print(f"Advertencia: Error durante flush/fsync final del log: {flush_err}", file=sys.stderr) print(f"Advertencia: Error durante flush/fsync final del log: {flush_err}", file=sys.stderr)
# <-- FIN NUEVO --> # <-- FIN NUEVO -->
print(f"\n{final_console_message} Consulta '{LOG_FILENAME}' para detalles.") # Mensaje final ya impreso antes del flush
sys.exit(exit_code) # Salir con el código apropiado sys.exit(exit_code) # Salir con el código apropiado

View File

@ -1,3 +1,9 @@
"""
LadderToSCL - Conversor de Siemens LAD/FUP XML a SCL
Este script convierte archivos XML de Siemens LAD/FUP a un formato JSON simplificado.
"""
# ToUpload/x1_to_json.py # ToUpload/x1_to_json.py
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import json import json
@ -7,9 +13,15 @@ import sys
import traceback import traceback
import importlib import importlib
from lxml import etree from lxml import etree
from lxml.etree import XMLSyntaxError as etree_XMLSyntaxError # Alias para evitar conflicto
from collections import defaultdict from collections import defaultdict
import copy import copy
import time # <-- NUEVO: Para obtener metadatos import time # <-- NUEVO: Para obtener metadatos
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
# Importar funciones comunes y namespaces desde el nuevo módulo de utils # Importar funciones comunes y namespaces desde el nuevo módulo de utils
try: try:
@ -209,12 +221,18 @@ def load_parsers(parsers_dir="parsers"):
return parser_map return parser_map
def convert_xml_to_json(xml_filepath, json_filepath, parser_map): # <-- MODIFICADO: parser_map ya no es un argumento, se carga dentro -->
def convert_xml_to_json(xml_filepath, json_filepath):
""" """
Convierte XML a JSON, detectando tipo, añadiendo metadatos del XML Convierte XML a JSON, detectando tipo, añadiendo metadatos del XML
y extrayendo comentarios/títulos de red de forma centralizada. (v3) y extrayendo comentarios/títulos de red de forma centralizada. (v3)
Carga los parsers necesarios internamente.
""" """
print(f"Iniciando conversión de '{xml_filepath}' a '{json_filepath}'...") print(f"Iniciando conversión de '{xml_filepath}' a '{json_filepath}'...")
# <-- NUEVO: Cargar parsers aquí -->
print("Cargando parsers de red...")
parser_map = load_parsers()
# <-- FIN NUEVO -->
if not os.path.exists(xml_filepath): if not os.path.exists(xml_filepath):
print(f"Error Crítico: Archivo XML no encontrado: '{xml_filepath}'") print(f"Error Crítico: Archivo XML no encontrado: '{xml_filepath}'")
return False return False
@ -438,7 +456,7 @@ def convert_xml_to_json(xml_filepath, json_filepath, parser_map):
print("Error Crítico: No se generó ningún resultado para el archivo XML.") print("Error Crítico: No se generó ningún resultado para el archivo XML.")
return False return False
except etree.XMLSyntaxError as e: except etree_XMLSyntaxError as e: # Usar alias
print(f"Error Crítico: Sintaxis XML inválida en '{xml_filepath}'. Detalles: {e}") print(f"Error Crítico: Sintaxis XML inválida en '{xml_filepath}'. Detalles: {e}")
return False return False
except Exception as e: except Exception as e:
@ -448,46 +466,49 @@ def convert_xml_to_json(xml_filepath, json_filepath, parser_map):
# --- Punto de Entrada Principal (__main__) --- # --- Punto de Entrada Principal (__main__) ---
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser( # Lógica para ejecución standalone
description="Convert Simatic XML (FC/FB/OB/DB/UDT/TagTable) to simplified JSON using dynamic parsers and add XML metadata." try:
) import tkinter as tk
parser.add_argument( from tkinter import filedialog
"xml_filepath", except ImportError:
help="Path to the input XML file passed from the main script (x0_main.py).", print("Error: Tkinter no está instalado. No se puede mostrar el diálogo de archivo.", file=sys.stderr)
) # No salimos, podríamos intentar obtener el path de otra forma o fallar más adelante
args = parser.parse_args() tk = None # Marcar como no disponible
xml_input_file = args.xml_filepath
if not os.path.exists(xml_input_file): xml_input_file = ""
print( if tk:
f"Error Crítico (x1): Archivo XML no encontrado: '{xml_input_file}'", root = tk.Tk()
file=sys.stderr, root.withdraw() # Ocultar la ventana principal de Tkinter
print("Por favor, selecciona el archivo XML de entrada...")
xml_input_file = filedialog.askopenfilename(
title="Selecciona el archivo XML de entrada",
filetypes=[("XML files", "*.xml"), ("All files", "*.*")]
) )
sys.exit(1) root.destroy() # Cerrar Tkinter
loaded_parsers = load_parsers() if not xml_input_file:
if not loaded_parsers: print("No se seleccionó ningún archivo. Saliendo.", file=sys.stderr)
print( # sys.exit(1) # No usar sys.exit aquí
"Advertencia (x1): No se cargaron parsers de red. Se continuará para UDT/TagTable/DB."
)
xml_filename_base = os.path.splitext(os.path.basename(xml_input_file))[0]
base_dir = os.path.dirname(xml_input_file)
output_dir = os.path.join(base_dir, "parsing")
os.makedirs(output_dir, exist_ok=True)
json_output_file = os.path.join(output_dir, f"{xml_filename_base}.json")
print(
f"(x1) Convirtiendo: '{os.path.relpath(xml_input_file)}' -> '{os.path.relpath(json_output_file)}'"
)
success = convert_xml_to_json(xml_input_file, json_output_file, loaded_parsers)
if success:
sys.exit(0)
else: else:
print( print(
f"\nError durante la conversión de '{os.path.relpath(xml_input_file)}'.", f"Archivo XML seleccionado: {xml_input_file}"
file=sys.stderr,
) )
sys.exit(1)
# Calcular ruta de salida JSON
xml_filename_base = os.path.splitext(os.path.basename(xml_input_file))[0]
base_dir = os.path.dirname(xml_input_file)
output_dir = os.path.join(base_dir, "parsing")
os.makedirs(output_dir, exist_ok=True)
json_output_file = os.path.join(output_dir, f"{xml_filename_base}.json")
print(
f"(x1 - Standalone) Convirtiendo: '{os.path.relpath(xml_input_file)}' -> '{os.path.relpath(json_output_file)}'"
)
# Llamar a la función principal (que ahora carga los parsers)
success = convert_xml_to_json(xml_input_file, json_output_file)
if success:
print("\nConversión completada exitosamente.")
else:
print(f"\nError durante la conversión de '{os.path.relpath(xml_input_file)}'.", file=sys.stderr)

View File

@ -1,3 +1,10 @@
"""
LadderToSCL - Conversor de Siemens LAD/FUP XML a SCL
Este script convierte un archivo JSON simplificado (resultado de un análisis de un XML de Siemens) a un
JSON enriquecido con lógica SCL. Se enfoca en la lógica de programación y la agrupación de instrucciones IF.
"""
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import json import json
import argparse import argparse
@ -8,6 +15,11 @@ import re
import importlib import importlib
import sys import sys
import sympy import sympy
script_root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
)
sys.path.append(script_root)
from backend.script_utils import load_configuration
# Import necessary components from processors directory # Import necessary components from processors directory
from processors.processor_utils import format_variable_name, sympy_expr_to_scl from processors.processor_utils import format_variable_name, sympy_expr_to_scl
@ -520,57 +532,54 @@ def process_json_to_scl(json_filepath, output_json_filepath):
# --- Ejecución (MODIFICADO) --- # --- Ejecución (MODIFICADO) ---
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser( # Lógica para ejecución standalone
description="Process simplified JSON to embed SCL logic, copying XML metadata. Expects original XML filepath." try:
) # <-- MODIFICADO import tkinter as tk
parser.add_argument( from tkinter import filedialog
"source_xml_filepath", except ImportError:
help="Path to the original source XML file (passed from x0_main.py).", print("Error: Tkinter no está instalado. No se puede mostrar el diálogo de archivo.", file=sys.stderr)
) tk = None
args = parser.parse_args()
source_xml_file = args.source_xml_filepath
if not os.path.exists(source_xml_file): input_json_file = ""
print( if tk:
f"Advertencia (x2): Archivo XML original no encontrado: '{source_xml_file}', pero se intentará encontrar el JSON correspondiente.", root = tk.Tk()
file=sys.stderr, root.withdraw()
print("Por favor, selecciona el archivo JSON de entrada (generado por x1)...")
input_json_file = filedialog.askopenfilename(
title="Selecciona el archivo JSON de entrada (.json)",
filetypes=[("JSON files", "*.json"), ("All files", "*.*")]
) )
# No salir, intentar encontrar el JSON de todas formas root.destroy()
xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0] if not input_json_file:
base_dir = os.path.dirname(source_xml_file) print("No se seleccionó ningún archivo. Saliendo.", file=sys.stderr)
parsing_dir = os.path.join(base_dir, "parsing")
# x2 LEE el .json y ESCRIBE el _processed.json
input_json_file = os.path.join(parsing_dir, f"{xml_filename_base}.json")
output_json_file = os.path.join(parsing_dir, f"{xml_filename_base}_processed.json")
os.makedirs(parsing_dir, exist_ok=True)
print(
f"(x2) Procesando: '{os.path.relpath(input_json_file)}' -> '{os.path.relpath(output_json_file)}'"
)
if not os.path.exists(input_json_file):
print(
f"Error Fatal (x2): El archivo de entrada JSON no existe: '{input_json_file}'",
file=sys.stderr,
)
print(
f"Asegúrate de que 'x1_to_json.py' se ejecutó correctamente para '{os.path.relpath(source_xml_file)}'.",
file=sys.stderr,
)
sys.exit(1)
else: else:
print(f"Archivo JSON de entrada seleccionado: {input_json_file}")
# Calcular ruta de salida JSON procesado
json_filename_base = os.path.splitext(os.path.basename(input_json_file))[0]
# Asumimos que el _processed.json va al mismo directorio 'parsing'
parsing_dir = os.path.dirname(input_json_file)
output_json_file = os.path.join(parsing_dir, f"{json_filename_base}_processed.json")
# Asegurarse de que el directorio de salida exista (aunque debería si el input existe)
os.makedirs(parsing_dir, exist_ok=True)
print(
f"(x2 - Standalone) Procesando: '{os.path.relpath(input_json_file)}' -> '{os.path.relpath(output_json_file)}'"
)
try: try:
success = process_json_to_scl(input_json_file, output_json_file) success = process_json_to_scl(input_json_file, output_json_file)
if success: if success:
sys.exit(0) print("\nProcesamiento completado exitosamente.")
else: else:
sys.exit(1) print(f"\nError durante el procesamiento de '{os.path.relpath(input_json_file)}'.", file=sys.stderr)
# sys.exit(1) # No usar sys.exit
except Exception as e: except Exception as e:
print( print(
f"Error Crítico (x2) durante el procesamiento de '{input_json_file}': {e}", f"Error Crítico (x2) durante el procesamiento de '{input_json_file}': {e}",
file=sys.stderr, file=sys.stderr,
) )
traceback.print_exc(file=sys.stderr) traceback.print_exc(file=sys.stderr)
sys.exit(1) # sys.exit(1) # No usar sys.exit

View File

@ -1,3 +1,9 @@
"""
LadderToSCL - Conversor de Siemens LAD/FUP XML a SCL
Este script es parte de un conjunto de herramientas para convertir proyectos de Siemens LAD/FUP a SCL.
"""
# ToUpload/x3_generate_scl.py # ToUpload/x3_generate_scl.py
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import json import json
@ -6,6 +12,11 @@ import re
import argparse import argparse
import sys import sys
import traceback 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
# --- Importar Generadores Específicos --- # --- Importar Generadores Específicos ---
try: try:
@ -25,7 +36,7 @@ except ImportError as e:
sys.exit(1) sys.exit(1)
# --- Constantes --- # --- Constantes ---
SCL_OUTPUT_DIRNAME = "scl_output" # <-- NUEVO: Nombre del directorio de salida final # SCL_OUTPUT_DIRNAME = "scl_output" # <-- Ya no se usa directamente en __main__, se lee de config
# --- Modificar generate_scl_or_markdown para usar el nuevo directorio de salida --- # --- Modificar generate_scl_or_markdown para usar el nuevo directorio de salida ---
@ -132,68 +143,82 @@ def generate_scl_or_markdown(
# --- Ejecución (MODIFICADO para usar SCL_OUTPUT_DIRNAME) --- # --- Ejecución (MODIFICADO para usar SCL_OUTPUT_DIRNAME) ---
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser( # Lógica para ejecución standalone
description=f"Generate final SCL/Markdown file into '{SCL_OUTPUT_DIRNAME}/'."
) # <-- MODIFICADO
parser.add_argument(
"source_xml_filepath", help="Path to the original source XML file."
)
parser.add_argument(
"project_root_dir",
help="Path to the root directory of the XML project structure.",
)
args = parser.parse_args()
source_xml_file = args.source_xml_filepath
project_root_dir = args.project_root_dir
if not os.path.exists(source_xml_file):
print(
f"Advertencia (x3): Archivo XML original no encontrado: '{source_xml_file}'. Se intentará continuar.",
file=sys.stderr,
)
# No salir necesariamente, podríamos tener el JSON procesado
xml_filename_base = os.path.splitext(os.path.basename(source_xml_file))[0]
xml_dir = os.path.dirname(source_xml_file)
parsing_dir = os.path.join(xml_dir, "parsing")
input_json_file = os.path.join(parsing_dir, f"{xml_filename_base}_processed.json")
# <-- MODIFICADO: Calcular directorio de salida final -->
# Siempre será 'scl_output' bajo la raíz del proyecto
final_output_dir = os.path.join(project_root_dir, SCL_OUTPUT_DIRNAME)
# <-- FIN MODIFICADO -->
print(f"(x3) Generando SCL/MD desde: '{os.path.relpath(input_json_file)}'")
print(f"(x3) Directorio de salida final: '{os.path.relpath(final_output_dir)}'")
print(f"(x3) Usando ruta raíz del proyecto: '{project_root_dir}' para buscar UDTs.")
# Asegurar que el directorio de salida final exista ANTES de llamar a la función
try: try:
os.makedirs(final_output_dir, exist_ok=True) import tkinter as tk
except OSError as e: from tkinter import filedialog
print( except ImportError:
f"Error Crítico (x3): No se pudo crear el directorio de salida '{final_output_dir}': {e}", print("Error: Tkinter no está instalado. No se puede mostrar el diálogo de archivo.", file=sys.stderr)
file=sys.stderr, tk = None
)
sys.exit(1)
if not os.path.exists(input_json_file): input_json_file = ""
print( project_root_dir = ""
f"Error Fatal (x3): JSON procesado no encontrado: '{input_json_file}'",
file=sys.stderr, if tk:
root = tk.Tk()
root.withdraw()
print("Por favor, selecciona el archivo JSON procesado de entrada (generado por x2)...")
input_json_file = filedialog.askopenfilename(
title="Selecciona el archivo JSON procesado de entrada (_processed.json)",
filetypes=[("Processed JSON files", "*_processed.json"), ("JSON files", "*.json"), ("All files", "*.*")]
) )
sys.exit(1) if input_json_file:
else: print(f"Archivo JSON procesado seleccionado: {input_json_file}")
try: print("Por favor, selecciona el directorio raíz del proyecto XML (ej. la carpeta 'PLC')...")
# Pasar el directorio de salida FINAL y la ruta raíz project_root_dir = filedialog.askdirectory(
success = generate_scl_or_markdown( title="Selecciona el directorio raíz del proyecto XML"
input_json_file, final_output_dir, project_root_dir )
) # <-- MODIFICADO if project_root_dir:
if success: print(f"Directorio raíz del proyecto seleccionado: {project_root_dir}")
sys.exit(0)
else: else:
sys.exit(1) # La función ya imprimió el error print("No se seleccionó directorio raíz. Saliendo.", file=sys.stderr)
except Exception as e: else:
print(f"Error Crítico no manejado en x3: {e}", file=sys.stderr) print("No se seleccionó archivo JSON procesado. Saliendo.", file=sys.stderr)
traceback.print_exc(file=sys.stderr) root.destroy()
sys.exit(1)
if input_json_file and project_root_dir:
# Calcular directorio de salida final
# <-- NUEVO: Leer nombre del directorio de salida desde la configuración -->
configs = load_configuration()
xml_parser_config = configs.get("XML Parser to SCL", {})
cfg_scl_output_dirname = xml_parser_config.get("scl_output_dir", "scl_output") # Leer con default
# <-- FIN NUEVO -->
final_output_dir = os.path.join(project_root_dir, cfg_scl_output_dirname) # Usar valor leído
print(f"(x3 - Standalone) Generando SCL/MD desde: '{os.path.relpath(input_json_file)}'")
print(f"(x3 - Standalone) Directorio de salida final: '{os.path.relpath(final_output_dir)}'")
print(f"(x3 - Standalone) Usando ruta raíz del proyecto: '{project_root_dir}' para buscar UDTs.")
# Asegurar que el directorio de salida final exista
try:
os.makedirs(final_output_dir, exist_ok=True)
except OSError as e:
print(
f"Error Crítico (x3): No se pudo crear el directorio de salida '{final_output_dir}': {e}",
file=sys.stderr,
)
# sys.exit(1) # No usar sys.exit
success = False # Marcar como fallo para evitar la llamada
else:
success = True # Marcar como éxito para proceder
if success: # Solo intentar si se pudo crear el directorio
try:
# Llamar a la función principal
success = generate_scl_or_markdown(
input_json_file, final_output_dir, project_root_dir
)
if success:
print("\nGeneración de SCL/MD completada exitosamente.")
else:
# La función generate_scl_or_markdown ya imprime el error
print(f"\nError durante la generación desde '{os.path.relpath(input_json_file)}'.", file=sys.stderr)
# sys.exit(1) # No usar sys.exit
except Exception as e:
print(f"Error Crítico no manejado en x3: {e}", file=sys.stderr)
traceback.print_exc(file=sys.stderr)
# sys.exit(1) # No usar sys.exit
else:
# Mensajes de cancelación ya impresos si aplica
pass

View File

@ -1,3 +1,9 @@
"""
LadderToSCL - Conversor de Siemens LAD/FUP XML a SCL
Este script genera documentacion MD de Cross Reference para Obsidian
"""
# ToUpload/x4_cross_reference.py # ToUpload/x4_cross_reference.py
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import json import json
@ -10,6 +16,11 @@ import re
import urllib.parse import urllib.parse
import shutil # <-- NUEVO: Para copiar archivos import shutil # <-- NUEVO: Para copiar archivos
from collections import defaultdict from collections import defaultdict
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
# --- Importar format_variable_name (sin cambios) --- # --- Importar format_variable_name (sin cambios) ---
try: try:
@ -40,14 +51,14 @@ except ImportError:
# --- Constantes --- # --- Constantes ---
SCL_OUTPUT_DIRNAME = "scl_output" # SCL_OUTPUT_DIRNAME = "scl_output" # Se leerá de config
XREF_SOURCE_SUBDIR = "source" # <-- NUEVO: Subdirectorio para fuentes MD # XREF_SOURCE_SUBDIR = "source" # Se leerá de config
CALL_XREF_FILENAME = "xref_calls_tree.md" # CALL_XREF_FILENAME = "xref_calls_tree.md" # Se leerá de config
DB_USAGE_XREF_FILENAME = "xref_db_usage_summary.md" # DB_USAGE_XREF_FILENAME = "xref_db_usage_summary.md" # Se leerá de config
PLC_TAG_XREF_FILENAME = "xref_plc_tags_summary.md" # PLC_TAG_XREF_FILENAME = "xref_plc_tags_summary.md" # Se leerá de config
MAX_CALL_DEPTH = 5 # MAX_CALL_DEPTH = 5 # Se leerá de config
INDENT_STEP = " " INDENT_STEP = " "
MAX_USERS_LIST = 20 # MAX_USERS_LIST = 20 # Se leerá de config
# --- Funciones de Análisis (find_calls_in_scl, find_db_tag_usage, find_plc_tag_usage sin cambios) --- # --- Funciones de Análisis (find_calls_in_scl, find_db_tag_usage, find_plc_tag_usage sin cambios) ---
@ -212,13 +223,14 @@ def find_plc_tag_usage(scl_code, plc_tag_names_set):
# <-- NUEVA FUNCION --> # <-- NUEVA FUNCION -->
def copy_and_prepare_source_files(project_root_dir, xref_output_dir): def copy_and_prepare_source_files(project_root_dir, xref_output_dir, scl_output_dirname, xref_source_subdir):
""" """
Copia archivos .scl y .md desde scl_output a xref_output/source, Copia archivos .scl y .md desde scl_output a xref_output/source,
convirtiendo .scl a .md con formato de bloque de código. convirtiendo .scl a .md con formato de bloque de código.
Usa los nombres de directorios pasados como argumentos.
""" """
scl_source_dir = os.path.join(project_root_dir, SCL_OUTPUT_DIRNAME) scl_source_dir = os.path.join(project_root_dir, scl_output_dirname)
md_target_dir = os.path.join(xref_output_dir, XREF_SOURCE_SUBDIR) md_target_dir = os.path.join(xref_output_dir, xref_source_subdir)
if not os.path.isdir(scl_source_dir): if not os.path.isdir(scl_source_dir):
print( print(
@ -293,7 +305,7 @@ def copy_and_prepare_source_files(project_root_dir, xref_output_dir):
# <-- MODIFICADO: get_scl_link --> # <-- MODIFICADO: get_scl_link -->
def get_scl_link( def get_scl_link(
block_name, block_entry, base_xref_dir block_name, block_entry, xref_source_subdir
): # Ya no necesita project_root_dir ): # Ya no necesita project_root_dir
""" """
Genera un enlace Markdown relativo al archivo .md correspondiente DENTRO de xref_output/source. Genera un enlace Markdown relativo al archivo .md correspondiente DENTRO de xref_output/source.
@ -302,10 +314,10 @@ def get_scl_link(
return f"`{block_name}`" return f"`{block_name}`"
# El nombre del archivo destino siempre será .md # El nombre del archivo destino siempre será .md
md_filename = format_variable_name(block_name) + ".md" md_filename = format_variable_name(block_name) + ".md" # Asegurar que format_variable_name esté disponible
# La ruta siempre estará dentro del subdirectorio 'source' # La ruta siempre estará dentro del subdirectorio fuente de xref
link_target_path = f"{XREF_SOURCE_SUBDIR}/{md_filename}" link_target_path = f"{xref_source_subdir}/{md_filename}"
# Codificar para URL/Markdown # Codificar para URL/Markdown
try: try:
@ -320,7 +332,7 @@ def get_scl_link(
# <-- MODIFICADO: build_call_tree_recursive (ya no necesita project_root_dir) --> # <-- MODIFICADO: build_call_tree_recursive (ya no necesita project_root_dir) -->
def build_call_tree_recursive( def build_call_tree_recursive( # Añadido max_call_depth, xref_source_subdir
current_node, current_node,
call_graph, call_graph,
block_data, block_data,
@ -328,6 +340,8 @@ def build_call_tree_recursive(
visited_in_path, visited_in_path,
base_xref_dir, base_xref_dir,
current_depth=0, current_depth=0,
max_call_depth=5,
xref_source_subdir="source"
): ):
""" """
Función recursiva para construir el árbol de llamadas indentado CON ENLACES Función recursiva para construir el árbol de llamadas indentado CON ENLACES
@ -336,10 +350,10 @@ def build_call_tree_recursive(
indent = INDENT_STEP * current_depth indent = INDENT_STEP * current_depth
block_entry = block_data.get(current_node) block_entry = block_data.get(current_node)
# Llamar a get_scl_link modificado # Llamar a get_scl_link modificado
node_link = get_scl_link(current_node, block_entry, base_xref_dir) node_link = get_scl_link(current_node, block_entry, xref_source_subdir)
output_lines.append(f"{indent}- {node_link}") output_lines.append(f"{indent}- {node_link}")
if current_depth >= MAX_CALL_DEPTH: if current_depth >= max_call_depth:
output_lines.append( output_lines.append(
f"{indent}{INDENT_STEP}[... Profundidad máxima alcanzada ...]" f"{indent}{INDENT_STEP}[... Profundidad máxima alcanzada ...]"
) )
@ -359,20 +373,22 @@ def build_call_tree_recursive(
block_data, block_data,
output_lines, output_lines,
visited_in_path.copy(), visited_in_path.copy(),
base_xref_dir, base_xref_dir, # base_xref_dir no se usa en la recursión, podría quitarse
current_depth + 1, current_depth + 1,
max_call_depth=max_call_depth, # Pasar parámetro
xref_source_subdir=xref_source_subdir # Pasar parámetro
) )
# <-- MODIFICADO: generate_call_tree_output (ya no necesita project_root_dir) --> # <-- MODIFICADO: generate_call_tree_output (ya no necesita project_root_dir) -->
def generate_call_tree_output(call_graph, block_data, base_xref_dir): def generate_call_tree_output(call_graph, block_data, base_xref_dir, max_call_depth, xref_source_subdir): # Añadido max_call_depth, xref_source_subdir
""" """
Genera las líneas de texto para el archivo de árbol de llamadas CON ENLACES Genera las líneas de texto para el archivo de árbol de llamadas CON ENLACES
a los archivos .md en xref_output/source. a los archivos .md en xref_output/source.
""" """
output_lines = ["# Árbol de Referencias Cruzadas de Llamadas\n"] output_lines = ["# Árbol de Referencias Cruzadas de Llamadas\n"]
output_lines.append(f"(Profundidad máxima: {MAX_CALL_DEPTH})\n") output_lines.append(f"(Profundidad máxima: {MAX_CALL_DEPTH})\n")
root_nodes = sorted( root_nodes = sorted( # Encontrar OBs
[ [
name name
for name, data in block_data.items() for name, data in block_data.items()
@ -387,7 +403,7 @@ def generate_call_tree_output(call_graph, block_data, base_xref_dir):
for ob_name in root_nodes: for ob_name in root_nodes:
ob_entry = block_data.get(ob_name) ob_entry = block_data.get(ob_name)
ob_link = get_scl_link( ob_link = get_scl_link(
ob_name, ob_entry, base_xref_dir ob_name, ob_entry, xref_source_subdir
) # Llamar a get_scl_link modificado ) # Llamar a get_scl_link modificado
output_lines.append(f"\n### Iniciando desde: {ob_link}\n") output_lines.append(f"\n### Iniciando desde: {ob_link}\n")
build_call_tree_recursive( build_call_tree_recursive(
@ -396,8 +412,10 @@ def generate_call_tree_output(call_graph, block_data, base_xref_dir):
block_data, block_data,
output_lines, output_lines,
set(), set(),
base_xref_dir, base_xref_dir, # No se usa en recursión
current_depth=0, current_depth=0,
max_call_depth=max_call_depth, # Pasar parámetro
xref_source_subdir=xref_source_subdir # Pasar parámetro
) )
all_callers = set(call_graph.keys()) all_callers = set(call_graph.keys())
@ -416,7 +434,7 @@ def generate_call_tree_output(call_graph, block_data, base_xref_dir):
for block_name in unreached: for block_name in unreached:
block_entry = block_data.get(block_name) block_entry = block_data.get(block_name)
block_link = get_scl_link( block_link = get_scl_link(
block_name, block_entry, base_xref_dir block_name, block_entry, xref_source_subdir
) # Llamar a get_scl_link modificado ) # Llamar a get_scl_link modificado
output_lines.append(f"- {block_link}") output_lines.append(f"- {block_link}")
return output_lines return output_lines
@ -424,7 +442,7 @@ def generate_call_tree_output(call_graph, block_data, base_xref_dir):
# --- Funciones para Salida Resumida (generate_db_usage_summary_output, generate_plc_tag_summary_output SIN CAMBIOS) --- # --- Funciones para Salida Resumida (generate_db_usage_summary_output, generate_plc_tag_summary_output SIN CAMBIOS) ---
# (Se omiten por brevedad) # (Se omiten por brevedad)
def generate_db_usage_summary_output(db_users): def generate_db_usage_summary_output(db_users, max_users_list): # Añadido max_users_list
"""Genera las líneas para el archivo Markdown de resumen de uso de DBs.""" """Genera las líneas para el archivo Markdown de resumen de uso de DBs."""
output_lines = ["# Resumen de Uso de DB Globales por Bloque\n\n"] output_lines = ["# Resumen de Uso de DB Globales por Bloque\n\n"]
if not db_users: if not db_users:
@ -440,7 +458,7 @@ def generate_db_usage_summary_output(db_users):
output_lines.append("- No utilizado directamente.\n") output_lines.append("- No utilizado directamente.\n")
else: else:
output_lines.append("Utilizado por:\n") output_lines.append("Utilizado por:\n")
display_users = users_list[:MAX_USERS_LIST] display_users = users_list[:max_users_list] # Usar parámetro
remaining_count = len(users_list) - len(display_users) remaining_count = len(users_list) - len(display_users)
for user_block in display_users: for user_block in display_users:
output_lines.append(f"- `{user_block}`") output_lines.append(f"- `{user_block}`")
@ -450,7 +468,7 @@ def generate_db_usage_summary_output(db_users):
return output_lines return output_lines
def generate_plc_tag_summary_output(plc_tag_users): def generate_plc_tag_summary_output(plc_tag_users, max_users_list): # Añadido max_users_list
"""Genera las líneas para el archivo Markdown de resumen de uso de PLC Tags.""" """Genera las líneas para el archivo Markdown de resumen de uso de PLC Tags."""
output_lines = ["# Resumen de Uso de PLC Tags Globales por Bloque\n\n"] output_lines = ["# Resumen de Uso de PLC Tags Globales por Bloque\n\n"]
if not plc_tag_users: if not plc_tag_users:
@ -466,7 +484,7 @@ def generate_plc_tag_summary_output(plc_tag_users):
output_lines.append("- No utilizado.\n") output_lines.append("- No utilizado.\n")
else: else:
output_lines.append("Utilizado por:\n") output_lines.append("Utilizado por:\n")
display_users = users_list[:MAX_USERS_LIST] display_users = users_list[:max_users_list] # Usar parámetro
remaining_count = len(users_list) - len(display_users) remaining_count = len(users_list) - len(display_users)
for user_block in display_users: for user_block in display_users:
output_lines.append(f"- `{user_block}`") output_lines.append(f"- `{user_block}`")
@ -477,20 +495,33 @@ def generate_plc_tag_summary_output(plc_tag_users):
# --- Función Principal (MODIFICADA para llamar a copy_and_prepare_source_files) --- # --- Función Principal (MODIFICADA para llamar a copy_and_prepare_source_files) ---
def generate_cross_references(project_root_dir, output_dir): def generate_cross_references(
project_root_dir,
output_dir,
scl_output_dirname,
xref_source_subdir,
call_xref_filename,
db_usage_xref_filename,
plc_tag_xref_filename,
max_call_depth,
max_users_list
):
""" """
Genera archivos de referencias cruzadas y prepara archivos fuente (.md) Genera archivos de referencias cruzadas y prepara archivos fuente (.md)
para visualización en Obsidian. para visualización en Obsidian.
Utiliza los parámetros de configuración pasados como argumentos.
""" """
print(f"--- Iniciando Generación de Referencias Cruzadas y Fuentes MD (x4) ---") print(f"--- Iniciando Generación de Referencias Cruzadas y Fuentes MD (x4) ---")
print(f"Buscando archivos JSON procesados en: {project_root_dir}") print(f"Buscando archivos JSON procesados en: {project_root_dir}")
print(f"Directorio de salida XRef: {output_dir}") print(f"Directorio de salida XRef: {output_dir}")
print(f"Directorio fuente SCL/MD: {scl_output_dirname}")
print(f"Subdirectorio fuentes MD para XRef: {xref_source_subdir}")
output_dir_abs = os.path.abspath(output_dir) output_dir_abs = os.path.abspath(output_dir)
# <-- NUEVO: Crear directorio y preparar archivos fuente ANTES de generar XRefs --> # <-- NUEVO: Crear directorio y preparar archivos fuente ANTES de generar XRefs -->
copy_and_prepare_source_files(project_root_dir, output_dir_abs) # Pasar los nombres de directorios leídos de la config
copy_and_prepare_source_files(project_root_dir, output_dir_abs, scl_output_dirname, xref_source_subdir)
# <-- FIN NUEVO --> # <-- FIN NUEVO -->
json_files = glob.glob( json_files = glob.glob(
os.path.join(project_root_dir, "**", "*_processed.json"), recursive=True os.path.join(project_root_dir, "**", "*_processed.json"), recursive=True
) )
@ -577,14 +608,14 @@ def generate_cross_references(project_root_dir, output_dir):
# 3. Generar Archivos de Salida XRef (MODIFICADO para usar la nueva función de árbol) # 3. Generar Archivos de Salida XRef (MODIFICADO para usar la nueva función de árbol)
os.makedirs(output_dir_abs, exist_ok=True) os.makedirs(output_dir_abs, exist_ok=True)
call_xref_path = os.path.join(output_dir_abs, CALL_XREF_FILENAME) call_xref_path = os.path.join(output_dir_abs, call_xref_filename) # Usar parámetro
db_usage_xref_path = os.path.join(output_dir_abs, DB_USAGE_XREF_FILENAME) db_usage_xref_path = os.path.join(output_dir_abs, db_usage_xref_filename) # Usar parámetro
plc_tag_xref_path = os.path.join(output_dir_abs, PLC_TAG_XREF_FILENAME) plc_tag_xref_path = os.path.join(output_dir_abs, plc_tag_xref_filename) # Usar parámetro
print(f"Generando ÁRBOL XRef de llamadas en: {call_xref_path}") print(f"Generando ÁRBOL XRef de llamadas en: {call_xref_path}")
try: try:
# <-- MODIFICADO: Llamar a la nueva función sin project_root_dir --> # <-- MODIFICADO: Llamar a la nueva función sin project_root_dir -->
call_tree_lines = generate_call_tree_output( call_tree_lines = generate_call_tree_output( # Pasar parámetros
call_graph, block_data, output_dir_abs call_graph, block_data, output_dir_abs
) )
with open(call_xref_path, "w", encoding="utf-8") as f: with open(call_xref_path, "w", encoding="utf-8") as f:
@ -598,7 +629,7 @@ def generate_cross_references(project_root_dir, output_dir):
# Generar Resumen de Uso de DB (sin cambios aquí) # Generar Resumen de Uso de DB (sin cambios aquí)
print(f"Generando RESUMEN XRef de uso de DBs en: {db_usage_xref_path}") print(f"Generando RESUMEN XRef de uso de DBs en: {db_usage_xref_path}")
try: try:
db_summary_lines = generate_db_usage_summary_output(db_users) db_summary_lines = generate_db_usage_summary_output(db_users, max_users_list) # Pasar parámetro
with open(db_usage_xref_path, "w", encoding="utf-8") as f: with open(db_usage_xref_path, "w", encoding="utf-8") as f:
[f.write(line + "\n") for line in db_summary_lines] [f.write(line + "\n") for line in db_summary_lines]
except Exception as e: except Exception as e:
@ -611,7 +642,7 @@ def generate_cross_references(project_root_dir, output_dir):
# Generar Resumen de Uso de PLC Tags (sin cambios aquí) # Generar Resumen de Uso de PLC Tags (sin cambios aquí)
print(f"Generando RESUMEN XRef de uso de PLC Tags en: {plc_tag_xref_path}") print(f"Generando RESUMEN XRef de uso de PLC Tags en: {plc_tag_xref_path}")
try: try:
plc_tag_lines = generate_plc_tag_summary_output(plc_tag_users) plc_tag_lines = generate_plc_tag_summary_output(plc_tag_users, max_users_list) # Pasar parámetro
with open(plc_tag_xref_path, "w", encoding="utf-8") as f: with open(plc_tag_xref_path, "w", encoding="utf-8") as f:
[f.write(line + "\n") for line in plc_tag_lines] [f.write(line + "\n") for line in plc_tag_lines]
except Exception as e: except Exception as e:
@ -627,35 +658,53 @@ def generate_cross_references(project_root_dir, output_dir):
# --- Punto de Entrada (sin cambios) --- # --- Punto de Entrada (sin cambios) ---
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser( print("(x4 - Standalone) Ejecutando generación de referencias cruzadas...")
description="Genera refs cruzadas y prepara archivos fuente MD para Obsidian."
)
parser.add_argument("project_root_dir", help="Ruta dir raíz proyecto XML.")
parser.add_argument(
"-o",
"--output",
help="Directorio para guardar salida XRef (incluyendo subdir 'source').",
)
args = parser.parse_args()
if not os.path.isdir(args.project_root_dir):
print(
f"Error: Dir proyecto no existe: '{args.project_root_dir}'", file=sys.stderr
)
sys.exit(1)
if not args.output:
print(
"Error: Se requiere el argumento -o/--output para especificar el directorio de salida XRef.",
file=sys.stderr,
)
sys.exit(1)
output_destination = args.output # Cargar configuración para obtener rutas
success = generate_cross_references(args.project_root_dir, output_destination) configs = load_configuration()
if success: working_directory = configs.get("working_directory")
print(
f"Archivos XRef y fuentes MD generados en: {os.path.abspath(output_destination)}" # Acceder a la configuración específica del grupo
) group_config = configs.get("level2", {})
sys.exit(0)
# Leer parámetros con valores por defecto (usando los defaults del esquema como guía)
# Parámetros necesarios para x4
cfg_scl_output_dirname = group_config.get("scl_output_dir", "scl_output")
cfg_xref_output_dirname = group_config.get("xref_output_dir", "xref_output")
cfg_xref_source_subdir = group_config.get("xref_source_subdir", "source")
cfg_call_xref_filename = group_config.get("call_xref_filename", "xref_calls_tree.md")
cfg_db_usage_xref_filename = group_config.get("db_usage_xref_filename", "xref_db_usage_summary.md")
cfg_plc_tag_xref_filename = group_config.get("plc_tag_xref_filename", "xref_plc_tags_summary.md")
cfg_max_call_depth = group_config.get("max_call_depth", 5)
cfg_max_users_list = group_config.get("max_users_list", 20)
# Calcular rutas
if not working_directory:
print("Error: 'working_directory' no encontrado en la configuración.", file=sys.stderr)
# No usamos sys.exit(1)
else: else:
print("Hubo errores durante la generación de refs cruzadas.", file=sys.stderr) # Calcular rutas basadas en la configuración
sys.exit(1) plc_subdir_name = "PLC" # Asumir nombre estándar
project_root_dir = os.path.join(working_directory, plc_subdir_name)
xref_output_dir = os.path.join(project_root_dir, cfg_xref_output_dirname) # Usar nombre de dir leído
if not os.path.isdir(project_root_dir):
print(f"Error: Directorio del proyecto '{project_root_dir}' no encontrado.", file=sys.stderr)
else:
# Llamar a la función principal
success = generate_cross_references(
project_root_dir,
xref_output_dir,
cfg_scl_output_dirname,
cfg_xref_source_subdir,
cfg_call_xref_filename,
cfg_db_usage_xref_filename,
cfg_plc_tag_xref_filename,
cfg_max_call_depth,
cfg_max_users_list
)
if success:
print("\n(x4 - Standalone) Proceso completado exitosamente.")
else:
print("\n(x4 - Standalone) Proceso finalizado con errores.", file=sys.stderr)

View File

@ -1,3 +1,9 @@
"""
LadderToSCL - Conversor de Siemens LAD/FUP XML a SCL
Este script genera documentación en Markdown y SCL a partir de un proyecto XML de Siemens LAD/FUP.
"""
# ToUpload/x5_aggregate.py # ToUpload/x5_aggregate.py
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os import os
@ -5,29 +11,36 @@ import argparse
import sys import sys
import glob import glob
import traceback 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
# --- Constantes --- # --- Constantes ---
# Nombre del archivo de salida por defecto (se creará en el directorio raíz del proyecto) # Nombre del archivo de salida por defecto (se creará en el directorio raíz del proyecto)
AGGREGATED_FILENAME = "full_project_representation.md" # AGGREGATED_FILENAME = "full_project_representation.md" # Se leerá de config
# Directorio donde x4 guarda sus salidas (relativo al directorio raíz del proyecto) # Directorio donde x4 guarda sus salidas (relativo al directorio raíz del proyecto)
XREF_OUTPUT_SUBDIR = "xref_output" # XREF_OUTPUT_SUBDIR = "xref_output" # Se leerá de config
# SCL_OUTPUT_DIRNAME = "scl_output" # Se leerá de config
def aggregate_files(project_root_dir, output_filepath): def aggregate_outputs(project_root_dir, output_filepath, scl_output_dirname, xref_output_dirname): # Añadido scl_output_dirname, xref_output_dirname
""" """
Busca archivos .scl y .md generados y los agrega en un único archivo Markdown. Busca archivos .scl y .md generados y los agrega en un único archivo Markdown.
""" """
print(f"--- Iniciando Agregación de Archivos (x5) ---") print(f"--- Iniciando Agregación de Archivos (x5) ---")
print(f"Leyendo desde directorios: '{scl_output_dirname}' y '{xref_output_dirname}' (relativos a la raíz)")
print(f"Directorio Raíz del Proyecto: {project_root_dir}") print(f"Directorio Raíz del Proyecto: {project_root_dir}")
print(f"Archivo de Salida: {output_filepath}") print(f"Archivo de Salida: {output_filepath}")
# Patrones para buscar archivos generados # Patrones para buscar archivos generados
# Buscamos .scl en cualquier subdirectorio (generados por x3 junto a los XML) # Buscamos .scl en cualquier subdirectorio (generados por x3 junto a los XML)
scl_pattern = os.path.join(project_root_dir, "**", "*.scl") scl_pattern = os.path.join(project_root_dir, "**", "*.scl")
# Buscamos .md en cualquier subdirectorio (UDT/TagTable generados por x3) # Buscamos .md en cualquier subdirectorio (UDT/TagTable generados por x3, XRef por x4)
md_pattern_general = os.path.join(project_root_dir, "**", "*.md") md_pattern_general = os.path.join(project_root_dir, "**", "*.md")
# Buscamos .md específicamente en el directorio de salida de x4 # Directorio de salida de x4
xref_dir = os.path.join(project_root_dir, XREF_OUTPUT_SUBDIR) xref_dir_abs = os.path.join(project_root_dir, xref_output_dirname)
# xref_pattern = os.path.join(xref_dir, "*.md") # No es necesario, el general los incluye scl_dir_abs = os.path.join(project_root_dir, scl_output_dirname)
print(f"Buscando archivos SCL con patrón: {scl_pattern}") print(f"Buscando archivos SCL con patrón: {scl_pattern}")
print(f"Buscando archivos MD con patrón: {md_pattern_general}") print(f"Buscando archivos MD con patrón: {md_pattern_general}")
@ -35,16 +48,18 @@ def aggregate_files(project_root_dir, output_filepath):
scl_files = glob.glob(scl_pattern, recursive=True) scl_files = glob.glob(scl_pattern, recursive=True)
md_files = glob.glob(md_pattern_general, recursive=True) md_files = glob.glob(md_pattern_general, recursive=True)
# Filtrar los archivos de salida del propio x5 y los XRef para que no se incluyan dos veces # Filtrar los archivos para asegurar que provienen de los directorios esperados
# si el patrón general los captura y están en el directorio raíz # y excluir el archivo de salida del propio x5.
output_filename_base = os.path.basename(output_filepath) output_filename_base = os.path.basename(output_filepath)
scl_files_filtered = [f for f in scl_files if os.path.dirname(f).startswith(scl_dir_abs)]
md_files_filtered = [ md_files_filtered = [
f for f in md_files f for f in md_files
if os.path.basename(f) != output_filename_base # Excluir el archivo de salida if os.path.basename(f) != output_filename_base # Excluir el archivo de salida
# No es necesario excluir los XRef explícitamente si están en su subdir and (os.path.dirname(f).startswith(scl_dir_abs) or os.path.dirname(f).startswith(xref_dir_abs)) # Incluir MD de scl_output y xref_output
# and XREF_OUTPUT_SUBDIR not in os.path.relpath(f, project_root_dir).split(os.sep)
] ]
all_files = sorted(scl_files_filtered + md_files_filtered) # Combinar y ordenar alfabéticamente
all_files = sorted(scl_files + md_files_filtered) # Combinar y ordenar alfabéticamente all_files = sorted(scl_files + md_files_filtered) # Combinar y ordenar alfabéticamente
@ -96,42 +111,44 @@ def aggregate_files(project_root_dir, output_filepath):
traceback.print_exc(file=sys.stderr) traceback.print_exc(file=sys.stderr)
return False return False
# --- Punto de Entrada --- # --- Punto de Entrada ---
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser( print("(x5 - Standalone) Ejecutando agregación de salidas...")
description="Agrega archivos .scl y .md generados en un único archivo Markdown."
)
parser.add_argument(
"project_root_dir",
help="Ruta al directorio raíz del proyecto XML (donde se buscarán los archivos generados)."
)
parser.add_argument(
"-o", "--output",
help=f"Ruta completa para el archivo Markdown agregado (por defecto: '{AGGREGATED_FILENAME}' en project_root_dir)."
)
args = parser.parse_args() # Cargar configuración para obtener rutas
configs = load_configuration()
working_directory = configs.get("working_directory")
# Validar directorio de entrada # Acceder a la configuración específica del grupo
if not os.path.isdir(args.project_root_dir): group_config = configs.get("level2", {})
print(f"Error: El directorio del proyecto no existe: '{args.project_root_dir}'", file=sys.stderr)
sys.exit(1)
# Determinar ruta de salida # Leer parámetros con valores por defecto (usando los defaults del esquema como guía)
output_file = args.output # Parámetros necesarios para x5
if not output_file: cfg_scl_output_dirname = group_config.get("scl_output_dir", "scl_output")
output_file = os.path.join(args.project_root_dir, AGGREGATED_FILENAME) cfg_xref_output_dirname = group_config.get("xref_output_dir", "xref_output")
cfg_aggregated_filename = group_config.get("aggregated_filename", "full_project_representation.md")
if not working_directory:
print("Error: 'working_directory' no encontrado en la configuración.", file=sys.stderr)
else: else:
# Asegurarse de que el directorio de salida exista si se especifica una ruta completa # Calcular rutas basadas en la configuración
output_dir = os.path.dirname(output_file) plc_subdir_name = "PLC" # Asumir nombre estándar
if output_dir and not os.path.exists(output_dir): project_root_dir = os.path.join(working_directory, plc_subdir_name)
os.makedirs(output_dir) # El archivo agregado va al working_directory original
output_agg_file = os.path.join(working_directory, cfg_aggregated_filename) # Usar nombre de archivo leído
if not os.path.isdir(project_root_dir):
print(f"Error: Directorio del proyecto '{project_root_dir}' no encontrado.", file=sys.stderr)
else:
# Llamar a la función principal
# Pasar los nombres de directorios leídos
success = aggregate_outputs(
project_root_dir,
output_agg_file,
cfg_scl_output_dirname,
cfg_xref_output_dirname)
# Llamar a la función principal if success:
success = aggregate_files(args.project_root_dir, output_file) print("\n(x5 - Standalone) Proceso completado exitosamente.")
else:
if success: print("\n(x5 - Standalone) Proceso finalizado con errores.", file=sys.stderr)
sys.exit(0)
else:
sys.exit(1)

View File

@ -2,4 +2,4 @@
"input_dir": "D:/Datos/Entrada", "input_dir": "D:/Datos/Entrada",
"output_dir": "D:/Datos/Salida", "output_dir": "D:/Datos/Salida",
"batch_size": 50 "batch_size": 50
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,35 +1,21 @@
[23:43:07] Iniciando ejecución de x3.py en C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport... [17:15:12] Iniciando ejecución de x1.py en C:\Trabajo\SIDEL\EMAILs\I_ E5.007727 _ Evo On - SFSRFH300172 + SFSRFH300109 - ANDIA LACTEOS...
[23:43:07] --- AML (CAx Export) to Hierarchical JSON and Obsidian MD Converter (v28 - Working Directory Integration) --- [17:15:14] Working directory: C:\Trabajo\SIDEL\EMAILs\I_ E5.007727 _ Evo On - SFSRFH300172 + SFSRFH300109 - ANDIA LACTEOS
[23:43:07] Using Working Directory for Output: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport [17:15:14] Input directory: C:\Trabajo\SIDEL\EMAILs\I_ E5.007727 _ Evo On - SFSRFH300172 + SFSRFH300109 - ANDIA LACTEOS
[23:43:11] Input AML: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export.aml [17:15:14] Output directory: C:/Users/migue/OneDrive/Miguel/Obsidean/Trabajo/VM/04-SIDEL/00 - MASTER/EMAILs
[23:43:11] Output Directory: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport [17:15:14] Cronologia file: C:/Users/migue/OneDrive/Miguel/Obsidean/Trabajo/VM/04-SIDEL/00 - MASTER/EMAILs\cronologia.md
[23:43:11] Output JSON: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export.hierarchical.json [17:15:14] Attachments directory: C:\Trabajo\SIDEL\EMAILs\I_ E5.007727 _ Evo On - SFSRFH300172 + SFSRFH300109 - ANDIA LACTEOS\adjuntos
[23:43:11] Output Main Tree MD: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export_Hardware_Tree.md [17:15:14] Beautify rules file: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\EmailCrono\config\beautify_rules.json
[23:43:11] Output IO Debug Tree MD: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export_IO_Upward_Debug.md [17:15:14] Found 1 .eml files
[23:43:11] Processing AML file: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export.aml [17:15:14] Loaded 0 existing messages
[23:43:11] Pass 1: Found 203 InternalElement(s). Populating device dictionary... [17:15:14] Processing C:\Trabajo\SIDEL\EMAILs\I_ E5.007727 _ Evo On - SFSRFH300172 + SFSRFH300109 - ANDIA LACTEOS\I_ E5.007727 _ Evo On - SFSRFH300172 + SFSRFH300109 - ANDIA LACTEOS.eml
[23:43:11] Pass 2: Identifying PLCs and Networks (Refined v2)... [17:15:14] Aplicando reglas de prioridad 1
[23:43:11] Identified Network: PROFIBUS_1 (bcc6f2bd-3d71-4407-90f2-bccff6064051) Type: Profibus [17:15:14] Aplicando reglas de prioridad 2
[23:43:11] Identified Network: ETHERNET_1 (c6d49787-a076-4592-994d-876eea123dfd) Type: Ethernet/Profinet [17:15:14] Aplicando reglas de prioridad 3
[23:43:11] Identified PLC: PLC (a48e038f-0bcc-4b48-8373-033da316c62b) - Type: CPU 1516F-3 PN/DP OrderNo: 6ES7 516-3FP03-0AB0 [17:15:14] Aplicando reglas de prioridad 4
[23:43:11] Pass 3: Processing InternalLinks (Robust Network Mapping & IO)... [17:15:14] Estadísticas de procesamiento:
[23:43:11] Found 118 InternalLink(s). [17:15:14] - Total mensajes encontrados: 1
[23:43:11] Mapping Device/Node 'E1' (NodeID:1643b51f-7067-4565-8f8e-109a1a775fed, Addr:10.1.33.11) to Network 'ETHERNET_1' [17:15:14] - Mensajes únicos añadidos: 1
[23:43:11] --> Associating Network 'ETHERNET_1' with PLC 'PLC' (via Node 'E1' Addr: 10.1.33.11) [17:15:14] - Mensajes duplicados ignorados: 0
[23:43:11] Mapping Device/Node 'P1' (NodeID:5aff409b-2573-485f-82bf-0e08c9200086, Addr:1) to Network 'PROFIBUS_1' [17:15:14] Writing 1 messages to C:/Users/migue/OneDrive/Miguel/Obsidean/Trabajo/VM/04-SIDEL/00 - MASTER/EMAILs\cronologia.md
[23:43:11] --> Associating Network 'PROFIBUS_1' with PLC 'PLC' (via Node 'P1' Addr: 1) [17:15:14] Ejecución de x1.py finalizada (success). Duración: 0:00:01.628641.
[23:43:11] Mapping Device/Node 'PB1' (NodeID:c796e175-c770-43f0-8191-fc91996c0147, Addr:12) to Network 'PROFIBUS_1' [17:15:14] Log completo guardado en: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\EmailCrono\log_x1.txt
[23:43:11] Mapping Device/Node 'PB1' (NodeID:0b44f55a-63c1-49e8-beea-24dc5d3226e3, Addr:20) to Network 'PROFIBUS_1'
[23:43:11] Mapping Device/Node 'PB1' (NodeID:25cfc251-f946-40c5-992d-ad6387677acb, Addr:21) to Network 'PROFIBUS_1'
[23:43:11] Mapping Device/Node 'PB1' (NodeID:57999375-ec72-46ef-8ec2-6c3178e8acf8, Addr:22) to Network 'PROFIBUS_1'
[23:43:11] Mapping Device/Node 'PB1' (NodeID:54e8db6a-9443-41a4-a85b-cf0722c1d299, Addr:10) to Network 'PROFIBUS_1'
[23:43:11] Mapping Device/Node 'PB1' (NodeID:4786bab6-4097-4651-ac19-6cadfc7ea735, Addr:8) to Network 'PROFIBUS_1'
[23:43:11] Mapping Device/Node 'PB1' (NodeID:1f08afcb-111f-428f-915e-69363af1b09a, Addr:40) to Network 'PROFIBUS_1'
[23:43:11] Data extraction and structuring complete.
[23:43:11] Generating JSON output: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export.hierarchical.json
[23:43:11] JSON data written successfully.
[23:43:11] Markdown summary written to: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export_Hardware_Tree.md
[23:43:11] IO upward debug tree written to: C:\Trabajo\SIDEL\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\Reporte\IOExport\SAE196_c0.2_CAx_Export_IO_Upward_Debug.md
[23:43:11] Script finished.
[23:43:12] Ejecución de x3.py finalizada (success). Duración: 0:00:05.235415.
[23:43:12] Log completo guardado en: d:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\log_x3.txt

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -1,15 +1,16 @@
flask beautifulsoup4==4.13.4
flask-sock Flask==3.1.0
lxml flask_sock==0.7.0
pandas html2text==2025.4.15
google-cloud-translate langid==1.1.6
openai lxml==5.4.0
ollama mammoth==1.9.0
langid ollama==0.4.8
openpyxl openai==1.77.0
beautifulsoup4 openpyxl==3.1.5
requests pandas==2.2.3
mammoth protobuf==6.30.2
html2text pypandoc==1.15
pypandoc Requests==2.32.3
# siemens-tia-scripting # Requiere instalación especial de TIA Portal Openness siemens_tia_scripting==1.0.7
sympy==1.13.3

View File

@ -418,6 +418,12 @@ function createFieldEditor(key, field) {
class="w-full p-2 border rounded" class="w-full p-2 border rounded"
onchange="updateVisualSchema()"> onchange="updateVisualSchema()">
</div> </div>
<div>
<label class="block text-sm font-bold mb-2">Valor por Defecto</label>
<input type="text" value="${field.default !== undefined ? field.default : ''}"
class="w-full p-2 border rounded"
onchange="updateVisualSchema()">
</div>
</div> </div>
${field.enum ? ` ${field.enum ? `
<div class="enum-container mt-4"> <div class="enum-container mt-4">
@ -494,28 +500,55 @@ function updateVisualSchema() {
const inputs = field.getElementsByTagName('input'); const inputs = field.getElementsByTagName('input');
const select = field.getElementsByTagName('select')[0]; const select = field.getElementsByTagName('select')[0];
const key = inputs[0].value; const key = inputs[0].value;
const fieldType = select.value; // string, directory, number, boolean, enum
const title = inputs[1].value;
const description = inputs[2].value;
const defaultValueInput = inputs[3]; // El nuevo input de valor por defecto
const defaultValueString = defaultValueInput.value;
let propertyDefinition = {
type: fieldType === 'directory' || fieldType === 'enum' ? 'string' : fieldType, // El tipo base
title: title,
description: description
};
// Añadir formato específico si es directorio
if (select.value === 'directory') { if (select.value === 'directory') {
schema.properties[key] = { propertyDefinition.format = 'directory';
type: 'string',
format: 'directory',
title: inputs[1].value,
description: inputs[2].value
};
} else if (select.value === 'enum') {
schema.properties[key] = {
type: 'string',
title: inputs[1].value,
description: inputs[2].value,
enum: field.querySelector('textarea').value.split('\n').filter(v => v.trim())
};
} else {
schema.properties[key] = {
type: select.value,
title: inputs[1].value,
description: inputs[2].value
};
} }
// Añadir enum si es de tipo enum
if (select.value === 'enum') {
propertyDefinition.enum = field.querySelector('textarea').value.split('\n').filter(v => v.trim());
}
// Procesar y añadir el valor por defecto si se proporcionó
if (defaultValueString !== null && defaultValueString.trim() !== '') {
let typedDefaultValue = defaultValueString;
try {
if (propertyDefinition.type === 'number' || propertyDefinition.type === 'integer') {
typedDefaultValue = Number(defaultValueString);
if (isNaN(typedDefaultValue)) {
console.warn(`Valor por defecto inválido para número en campo '${key}': ${defaultValueString}. Se omitirá.`);
// No añadir default si no es un número válido
} else {
// Opcional: truncar si el tipo es integer
if (propertyDefinition.type === 'integer' && !Number.isInteger(typedDefaultValue)) {
typedDefaultValue = Math.trunc(typedDefaultValue);
}
propertyDefinition.default = typedDefaultValue;
}
} else if (propertyDefinition.type === 'boolean') {
typedDefaultValue = ['true', '1', 'yes', 'on'].includes(defaultValueString.toLowerCase());
propertyDefinition.default = typedDefaultValue;
} else { // string, enum, directory
propertyDefinition.default = typedDefaultValue; // Ya es string
}
} catch (e) {
console.error(`Error procesando valor por defecto para campo '${key}':`, e);
}
}
schema.properties[key] = propertyDefinition;
}); });
const jsonEditor = document.getElementById('json-editor'); const jsonEditor = document.getElementById('json-editor');
@ -960,6 +993,81 @@ function collectFormData(level) {
return data; return data;
} }
// Añade esta función al final de tu archivo static/js/script.js
function shutdownServer() {
if (confirm("¿Estás seguro de que quieres detener el servidor? La aplicación se cerrará.")) {
fetch('/_shutdown', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
alert("El servidor se está deteniendo. Puede que necesites cerrar esta pestaña manualmente.");
// Opcionalmente, puedes intentar cerrar la ventana/pestaña
// window.close(); // Esto puede no funcionar en todos los navegadores por seguridad
document.body.innerHTML = '<div class="alert alert-info">El servidor se ha detenido. Cierra esta ventana.</div>';
} else {
alert("Error al intentar detener el servidor: " + data.message);
}
})
.catch(error => {
// Es normal recibir un error de red aquí porque el servidor se está apagando
console.warn("Error esperado al detener el servidor (puede que ya se haya detenido):", error);
alert("Solicitud de detención enviada. El servidor debería detenerse. Cierra esta ventana.");
document.body.innerHTML = '<div class="alert alert-info">El servidor se está deteniendo. Cierra esta ventana.</div>';
});
}
}
// Asegúrate de que las funciones fetchLogs y clearLogs también estén definidas en este archivo si las usas.
// Ejemplo de fetchLogs y clearLogs (si no las tienes ya):
function fetchLogs() {
fetch('/api/logs')
.then(response => response.json())
.then(data => {
const logOutput = document.getElementById('log-output');
logOutput.textContent = data.logs || 'No hay logs.';
logOutput.scrollTop = logOutput.scrollHeight; // Scroll to bottom
})
.catch(error => console.error('Error fetching logs:', error));
}
function clearLogs() {
if (confirm("¿Estás seguro de que quieres borrar los logs?")) {
fetch('/api/logs', { method: 'DELETE' })
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
fetchLogs(); // Refresh logs after clearing
showToast('Logs borrados correctamente.');
} else {
showToast('Error al borrar los logs.', 'error');
}
})
.catch(error => {
console.error('Error clearing logs:', error);
showToast('Error de red al borrar los logs.', 'error');
});
}
}
// Necesitarás una función showToast o similar si la usas
function showToast(message, type = 'success') {
// Implementa tu lógica de Toast aquí
console.log(`Toast (${type}): ${message}`);
alert(`Toast (${type}): ${message}`); // Simple alert como placeholder
}
// Llama a fetchLogs al cargar la página si es necesario
// document.addEventListener('DOMContentLoaded', fetchLogs);
// Agregar función para guardar configuración // Agregar función para guardar configuración
async function saveConfig(level) { async function saveConfig(level) {
const saveButton = document.getElementById(`save-config-${level}`); const saveButton = document.getElementById(`save-config-${level}`);

View File

@ -68,6 +68,13 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Botón para detener el servidor -->
<div class="mt-8 pt-4 border-t border-gray-300">
<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>
</div> </div>
</div> </div>