From c8141deb630bf097c57d4cad9e4cfed0a81b7ac9 Mon Sep 17 00:00:00 2001 From: Miguel Date: Mon, 14 Jul 2025 10:29:47 +0200 Subject: [PATCH] =?UTF-8?q?Se=20actualizaron=20los=20archivos=20de=20confi?= =?UTF-8?q?guraci=C3=B3n=20JSON=20para=20reflejar=20nuevos=20par=C3=A1metr?= =?UTF-8?q?os=20de=20directorios=20de=20exportaci=C3=B3n,=20incluyendo=20"?= =?UTF-8?q?aml=5Fexp=5Fdirectory"=20y=20"resultados=5Fexp=5Fdirectory".=20?= =?UTF-8?q?Adem=C3=A1s,=20se=20realizaron=20mejoras=20en=20el=20script=20`?= =?UTF-8?q?x1=5Fexport=5FCAx.py`,=20optimizando=20la=20gesti=C3=B3n=20de?= =?UTF-8?q?=20directorios=20de=20salida=20y=20la=20detecci=C3=B3n=20de=20a?= =?UTF-8?q?rchivos=20de=20proyecto=20TIA.=20Se=20ajustaron=20los=20mensaje?= =?UTF-8?q?s=20de=20depuraci=C3=B3n=20y=20se=20mejor=C3=B3=20la=20estructu?= =?UTF-8?q?ra=20del=20c=C3=B3digo=20para=20mayor=20claridad.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../IO_adaptation/.cursor/rules/reglas.mdc | 4 + .../IO_adaptation/.doc/MemoriaDeEvolucion.md | 69 + .../IO_adaptation/.doc/backend_setup.md | 410 ++++++ backend/script_groups/IO_adaptation/data.json | 4 +- .../IO_adaptation/esquema_group.json | 17 +- .../IO_adaptation/esquema_work.json | 21 +- .../IO_adaptation/script_config.json | 14 +- .../IO_adaptation/x1_export_CAx.py | 138 +- .../IO_adaptation/x2_process_CAx.py | 1131 ++++++++++------- .../IO_adaptation/x3_excel_to_md.py | 139 +- .../IO_adaptation/x4_prompt_generator.py | 23 +- data/launcher_history.json | 13 + data/log.txt | 311 +---- templates/index.html | 2 +- 14 files changed, 1391 insertions(+), 905 deletions(-) create mode 100644 backend/script_groups/IO_adaptation/.cursor/rules/reglas.mdc create mode 100644 backend/script_groups/IO_adaptation/.doc/MemoriaDeEvolucion.md create mode 100644 backend/script_groups/IO_adaptation/.doc/backend_setup.md diff --git a/backend/script_groups/IO_adaptation/.cursor/rules/reglas.mdc b/backend/script_groups/IO_adaptation/.cursor/rules/reglas.mdc new file mode 100644 index 0000000..6e720b3 --- /dev/null +++ b/backend/script_groups/IO_adaptation/.cursor/rules/reglas.mdc @@ -0,0 +1,4 @@ +--- +alwaysApply: true +--- +Puedes usar .doc\MemoriaDeEvolucion.md para obtener un contexto de las ultimas modificaciones y conceptos sobre este proyecto. Quisiera que con los conocimientos importantes y desiciones importantes adquiridas en cada modificacion los agregues a MemoriaDeEvolucion.md manteniendo el estilo que ya tenemos de texto simple sin demasiado codigo y una semantica resumida. \ No newline at end of file diff --git a/backend/script_groups/IO_adaptation/.doc/MemoriaDeEvolucion.md b/backend/script_groups/IO_adaptation/.doc/MemoriaDeEvolucion.md new file mode 100644 index 0000000..ab80d1c --- /dev/null +++ b/backend/script_groups/IO_adaptation/.doc/MemoriaDeEvolucion.md @@ -0,0 +1,69 @@ +Definiciones de Frontend en .doc\backend_setup.md + + +### Flujo de trabajo: + +Se usa [x1] para exportar los datos de configuración desde el proyecto de Tia Portal, incluidos los IO configurados en el hardware. Esto genera un archivo AML. +Con [x2] se procesa el archivo AML y se convierte en markdown. Si hay un solo PLC se genera el archivo [Hardware.md]. **Además genera un archivo Excel con los IOs por nodos del PLC.** +3. Se deben procesar los IO **desde el esquema eléctrico y agregarlos** a [Hardware.md]. +Se debe exportar todos los tags como un archivo excel desde el Tia Portal. +Con [x3] se convierte y se filtran los tags según los path definidos en [io_paths_config] y se genera un archivo "Master IO Tags.md". +[x4] Genera el prompt a usar con Claude usando MCP para que pueda acceder a los archivos originales y evitar tener que hacer uploads. +Una vez que se genera el archivo procesado por el LLM se puede usar +[x5] que convierte las adaptaciones a un archivo que luego se puede importar en Tia Portal +8. Importar en Tia Portal el archivo [PLCTags_Updated.xlsx] + +**Nuevo flujo alternativo con Excel:** + +Después del paso 2, se puede usar : + [x7] para: Modificar manualmente el archivo Excel de IOs generado- Aplicar los cambios de vuelta al archivo AML original Generar un AML actualizado con las modificaciones + +### Cambios recientes: + +**Automatización de selección de proyecto TIA Portal (x1_export_CAx.py):** +- Eliminada la interfaz gráfica tkinter para selección manual de archivos +- El script ahora lee automáticamente la ruta del proyecto desde la configuración `siemens_tia_project` en work_dir.json +- Nueva función `find_project_file_in_dir()` que busca automáticamente archivos .ap18/.ap19/.ap20 en el directorio configurado +- Validación automática que verifica que exista exactamente un archivo de proyecto en el directorio especificado +- Mejora en la experiencia de usuario al eliminar pasos manuales de selección de archivos + +**Directorio de exportación CAx configurable (x1_export_CAx.py):** +- La ruta de exportación de archivos CAx ahora es configurable. +- El script utiliza el valor `aml_exp_directory` de la configuración `level2` en `work_dir.json`. +- La nueva ruta de salida se construye como `working_directory` / `aml_exp_directory`. +- Todos los archivos generados (AML, Markdown y log) se guardan en este directorio, manteniendo los artefactos de exportación organizados. + +**Directorio de E/S configurable para el procesamiento CAx (x2_process_CAx.py):** +- El script ahora utiliza el directorio `aml_exp_directory` de `work_dir.json` para la entrada y salida de archivos. +- Se ha unificado el comportamiento con `x1_export_CAx.py`, esperando el archivo AML de entrada en `working_directory` / `aml_exp_directory`. +- Todos los archivos generados (JSON, Markdown, Excel) se guardan en este mismo directorio, manteniendo la consistencia del flujo de trabajo. +- Se mantiene el fallback al directorio `.debug` para la ejecución independiente del script. + +**Selección automática de archivo AML (x2_process_CAx.py):** +- El script ahora busca automáticamente un único archivo `.aml` en el directorio `aml_exp_directory`. +- Si se encuentra un solo archivo, se utiliza directamente, eliminando la necesidad del diálogo de selección manual. +- Si se encuentran cero o múltiples archivos `.aml`, el script recurre al diálogo de selección manual para que el usuario resuelva la ambigüedad. +- Esta mejora agiliza el flujo de trabajo al automatizar la selección de archivos en el caso más común. + +**Directorio de resultados configurable (x1 y x2):** +- Se ha agregado el parámetro `resultados_exp_directory` a la configuración `level2` en `work_dir.json`. +- `x1_export_CAx.py` ahora guarda la tabla de resumen (`*CAx_Summary.md`) en el subdirectorio `working_directory` / `resultados_exp_directory`. +- `x2_process_CAx.py` guarda todos los archivos de resultados finales (tablas Markdown y reportes Excel) en `working_directory` / `resultados_exp_directory`, anidados en carpetas por PLC. +- Los archivos intermedios (JSON, logs, debug files) permanecen en `aml_exp_directory`, separando los artefactos de compilación de los resultados finales. + +**Corrección de errores de exportación en TIA Portal (x1_export_CAx.py):** +- Se ha solucionado un error que ocurría cuando el archivo AML a exportar ya existía. El script ahora elimina los archivos `.aml` y `.log` existentes antes de la exportación para permitir la sobrescritura. +- Se ha corregido el manejo de excepciones al eliminar un bloque `except` que intentaba capturar una excepción inexistente (`TiaException`), lo que causaba un error secundario y ocultaba el problema original. + +**Flujo de trabajo de E/S con Excel mejorado (x3_excel_to_md.py):** +- **Directorios configurables**: El script ahora utiliza `tags_exp_directory` (de `level3`) para buscar archivos Excel y `resultados_exp_directory` (de `level2`) para guardar el archivo Markdown resultante. Ambos se leen desde `work_dir.json`. +- **Selección automática de archivo**: Si solo existe un archivo `.xlsx` en `tags_exp_directory`, el script lo utiliza automáticamente, eliminando la necesidad de selección manual. +- **Fallback a diálogo manual**: Si se encuentran cero o múltiples archivos Excel, el script muestra un diálogo para que el usuario seleccione el archivo correcto, manteniendo la flexibilidad. +- **Consistencia del proyecto**: Esta actualización alinea el comportamiento de `x3_excel_to_md.py` con los scripts `x1` y `x2`, creando un flujo de trabajo más coherente y automatizado. + +**Configuración dinámica de rutas en el generador de prompts (x4_prompt_generator.py):** +- Las rutas de los archivos de entrada (`Master IO Tags.md` y `Hardware.md`) ahora se construyen dinámicamente utilizando `resultados_exp_directory` de la configuración `level2`, alineándose con el resto de los scripts. +- La configuración de las rutas base de Obsidian (`ObsideanDir`, `ObsideanProjectsBase`) se ha movido al `level3` para una mejor organización de la configuración. +- El prompt generado refleja automáticamente estas rutas configurables, asegurando que las herramientas de IA siempre utilicen las ubicaciones correctas de los archivos. + + diff --git a/backend/script_groups/IO_adaptation/.doc/backend_setup.md b/backend/script_groups/IO_adaptation/.doc/backend_setup.md new file mode 100644 index 0000000..f05cb45 --- /dev/null +++ b/backend/script_groups/IO_adaptation/.doc/backend_setup.md @@ -0,0 +1,410 @@ +# Guía de Configuración para Scripts Backend + +## Introducción + +Esta guía explica cómo usar la función `load_configuration()` para cargar parámetros desde un archivo `script_config.json` en los scripts del backend. + +## 1. Configuración del Script + +Para que tus scripts puedan encontrar los módulos del proyecto, necesitas añadir el directorio raíz al path de Python. + +### Path Setup e Importación + +Coloca este código al inicio de tu script: + +```python +import os +import sys + +# Añadir el directorio raíz al sys.path +# El número de `os.path.dirname()` depende de la profundidad del script. +# Para /backend/script_groups/grupo/script.py, son 4. +script_root = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.dirname(__file__))) +) +sys.path.append(script_root) + +# Importar la función +from backend.script_utils import load_configuration +``` + +## 2. Cargar la Configuración + +La función `load_configuration()` busca y carga un archivo llamado `script_config.json` que debe estar en el **mismo directorio** que el script que la ejecuta. Devuelve un diccionario con todo el contenido del JSON. + +### Ejemplo de Uso + +```python +def main(): + # Cargar configuraciones del archivo script_config.json + configs = load_configuration() + + # Es buena práctica verificar si la configuración se cargó + if not configs: + print("Error: No se pudo cargar la configuración. Saliendo.") + return + + # Acceder a los parámetros usando .get() para evitar errores + working_directory = configs.get("working_directory", "") + level1_config = configs.get("level1", {}) + level2_config = configs.get("level2", {}) + level3_config = configs.get("level3", {}) + + # Ejemplo de uso de un parámetro específico con valor por defecto + scl_output_dir = level2_config.get("scl_output_dir", "scl_output") + xref_output_dir = level2_config.get("xref_output_dir", "xref_output") + + print(f"Directorio de trabajo: {working_directory}") + print(f"Directorio de salida SCL: {scl_output_dir}") + +if __name__ == "__main__": + main() +``` + +## 3. Archivo `script_config.json` + +Este archivo contiene los parámetros de tu script. La estructura interna del JSON, como el uso de `"level1"`, `"level2"`, etc., es una convención para organizar los parámetros. `load_configuration()` simplemente lee el archivo y devuelve su contenido. + +**Ejemplo de `script_config.json`:** +```json +{ + "working_directory": "/ruta/al/directorio/de/trabajo", + "level1": { + "parametro_global_1": "valor1" + }, + "level2": { + "scl_output_dir": "scl_output", + "xref_output_dir": "xref_output" + }, + "level3": { + "parametro_especifico_1": true + } +} +``` + +## 4. Manejo de Errores + +`load_configuration()` está diseñada para ser robusta: +- Si `script_config.json` no se encuentra, retorna un diccionario vacío `{}`. +- Si el JSON es inválido, imprime un error y también retorna `{}`. + +**Siempre** comprueba si el diccionario devuelto está vacío para manejar estos casos de forma segura en tu script. + +## 5. Jerarquía de Archivos de Configuración + +El sistema utiliza un modelo de configuración en cascada para gestionar los parámetros de los scripts. Esta jerarquía permite establecer configuraciones a nivel global, de grupo y de trabajo. Antes de la ejecución, el launcher lee estos archivos, los combina y genera un único `script_config.json` en la carpeta del script. La función `load_configuration()` es la que finalmente lee este archivo consolidado. + +A continuación se describe la finalidad y ubicación de cada archivo clave. + +### Archivos de Valores (Parámetros) + +Contienen los datos y variables que utilizará el script. La configuración se superpone en el siguiente orden: `Nivel 1 < Nivel 2 < Nivel 3`. + +- **`data.json` (Nivel 1 - Global)** + - **Ubicación:** `data/data.json` + - **Utilidad:** Almacena variables globales disponibles para todos los scripts. Ideal para parámetros generales. + - **Acceso:** Sus datos se cargan en la clave `"level1"` del diccionario `configs`. + +- **`script_config.json` (Nivel 2 - Grupo)** + - **Ubicación:** En la raíz de cada directorio de grupo (ej: `backend/script_groups/MiGrupo/script_config.json`). + - **Utilidad:** Define parámetros compartidos por todos los scripts de un grupo. + - **Acceso:** Sus datos se cargan en la clave `"level2"`. + +- **`work_dir.json` (Nivel 3 - Directorio de Trabajo)** + - **Ubicación:** Dentro del directorio de trabajo que el script va a procesar. + - **Utilidad:** Contiene parámetros para una ejecución específica. Es el nivel más específico y sobrescribe los anteriores. + - **Acceso:** Sus datos se cargan en la clave `"level3"`. + +### Archivos de Esquema (Definiciones de Estructura) + +No contienen valores, sino que describen la estructura y tipos de datos que los archivos de valores deben tener. Son usados por el launcher para validación y para generar interfaces de configuración. + +- **`esquema_group.json`** + - **Ubicación:** Raíz del directorio del grupo. + - **Utilidad:** Define la estructura del `script_config.json` del grupo. + +- **`esquema_work.json`** + - **Ubicación:** Raíz del directorio del grupo. + - **Utilidad:** Define la estructura del `work_dir.json`. + +## 6. Documentación de Scripts para el Launcher + +El sistema de launcher utiliza archivos JSON para mostrar información sobre los grupos de scripts y scripts individuales en la interfaz web. + +### Archivo description.json (Descripción del Grupo) + +Ubicación: En el directorio raíz del grupo de scripts. + +```json +{ + "name": "Nombre del Grupo", + "description": "Descripción del propósito y funcionalidad del grupo", + "version": "1.0", + "author": "Nombre del Autor" +} +``` + +### Archivo scripts_description.json (Descripción de Scripts) + +Ubicación: En el directorio raíz del grupo de scripts. + +```json +{ + "nombre_script.py": { + "display_name": "Nombre para mostrar en la UI", + "short_description": "Descripción breve del script", + "long_description": "Descripción detallada con explicación completa de funcionalidad, pasos que ejecuta, y contexto de uso", + "hidden": false + }, + "script_interno.py": { + "display_name": "Script Interno", + "short_description": "Script de uso interno", + "long_description": "", + "hidden": true + } +} +``` + +### Propiedades Importantes + +- **hidden**: `true` oculta el script del launcher (útil para scripts auxiliares) +- **display_name**: Nombre amigable que aparece en la interfaz +- **short_description**: Se muestra en la lista de scripts +- **long_description**: Se muestra al expandir detalles del script + +### Ejemplo Práctico + +Para un grupo "XML Parser to SCL": + +**description.json:** +```json +{ + "name": "Siemens-Tia : 03 : Procesador de XML LAD-SCL-AWL", + "description": "Scripts que procesan archivos XML exportados de TIA, convirtiendo LAD a SCL", + "version": "1.0", + "author": "Miguel" +} +``` + +**scripts_description.json:** +```json +{ + "x0_main.py": { + "display_name": "1: Procesar Exportación XML completa", + "short_description": "Conversor principal de LAD/FUP XML a SCL", + "long_description": "Script orquestador que procesa todos los archivos XML...", + "hidden": false + }, + "x1_to_json.py": { + "display_name": "x1_to_json", + "short_description": "Converter XML interno", + "long_description": "", + "hidden": true + } +} +``` + +## 7. Requisitos de Codificación de Salida (stdout) + +Toda la salida estándar (`stdout`) generada por los scripts (por ejemplo, mediante la función `print()`) es capturada en tiempo real y mostrada en el panel de logs del frontend. + +Para garantizar que el texto se muestre correctamente y evitar caracteres corruptos (mojibake), **la salida de los scripts debe tener codificación UTF-8**. + +### Configuración Automática + +El sistema está diseñado para facilitar esto. Al ejecutar un script, el entorno se configura automáticamente para que la salida de Python sea UTF-8. Específicamente, se establece la variable de entorno `PYTHONIOENCODING=utf-8`. + +Gracias a esto, en la mayoría de los casos, **no necesitas hacer nada especial**. Simplemente usa `print()` y la codificación será la correcta. + +### Casos Especiales y Solución de Problemas + +Pueden surgir problemas si tu script lee datos de fuentes externas (como archivos) que tienen una codificación diferente. Si imprimes contenido directamente sin decodificarlo primero, podrías enviar bytes no válidos a la salida. + +La regla es: **decodifica los datos de entrada a un string de Python lo antes posible, y deja que `print()` se encargue de la codificación de salida.** + +**Ejemplo de cómo manejar un archivo con codificación `latin-1`:** + +```python +# INCORRECTO: Si el archivo no es UTF-8, esto puede enviar bytes inválidos. +# El launcher intentará decodificarlos como UTF-8 y podría fallar o mostrar basura. +try: + with open('mi_archivo_legacy.txt', 'rb') as f: + print(f.read()) +except Exception as e: + print(f"Esto probablemente falle o muestre texto corrupto: {e}") + + +# CORRECTO: Leer el archivo especificando su codificación para decodificarlo a un string. +# Una vez que es un string de Python, `print` lo manejará correctamente. +try: + with open('mi_archivo_legacy.txt', 'r', encoding='latin-1') as f: + contenido = f.read() + # Ahora `contenido` es un string de Python. + # print() lo codificará a UTF-8 automáticamente gracias a la configuración del entorno. + print(contenido) +except FileNotFoundError: + print("El archivo 'mi_archivo_legacy.txt' no fue encontrado para el ejemplo.") +except Exception as e: + print(f"Error al procesar el archivo: {e}") +``` + +## 8. Uso de Servicios Compartidos + +El proyecto ofrece una serie de servicios reutilizables en el directorio `services/` para tareas comunes como la manipulación de archivos Excel, detección de idioma o traducción. + +Para utilizar estos servicios en tu script, asegúrate de que el directorio raíz del proyecto esté en el `sys.path`, como se explica en la sección 1 de esta guía. + +### 8.1 Servicio de Excel (`ExcelService`) + +El `ExcelService` (`services/excel/excel_service.py`) facilita la lectura y escritura de archivos Excel, con manejo de reintentos (por si el archivo está abierto) y opciones de formato. + +**Ejemplo de importación y uso:** + +```python +# Asegúrate de tener el path raíz configurado +# ... (código de configuración de sys.path) + +from services.excel.excel_service import ExcelService + +def main(): + excel_service = ExcelService() + + # Leer un archivo Excel + try: + df = excel_service.read_excel("mi_archivo_de_entrada.xlsx") + print("Datos cargados exitosamente.") + + # ... procesar el DataFrame ... + + # Guardar el DataFrame con formato personalizado + format_options = { + 'freeze_row': 2, + 'header_color': 'E6E6E6' + } + + excel_service.save_excel( + df, + "mi_archivo_de_salida.xlsx", + sheet_name="Resultados", + format_options=format_options + ) + print("Archivo guardado con éxito.") + + except Exception as e: + print(f"Ocurrió un error al manejar el archivo Excel: {e}") + +if __name__ == "__main__": + main() +``` + +### 8.2 Servicios de Lenguaje + +Los servicios de lenguaje (`services/language/`) permiten detectar el idioma de un texto. + +**Ejemplo de importación y uso:** + +```python +# Asegúrate de tener el path raíz configurado +# ... (código de configuración de sys.path) + +from services.language.language_factory import LanguageFactory +from services.language.language_utils import LanguageUtils + +def main(): + # Crear el servicio de detección de idioma + allowed_languages = LanguageUtils.get_available_languages() + detector = LanguageFactory.create_service("langid", allowed_languages=allowed_languages) + + # Detectar idioma de un texto + text = "Este es un texto de ejemplo en español." + lang, confidence = detector.detect_language(text) + + print(f"Texto: '{text}'") + print(f"Idioma detectado: {LanguageUtils.get_language_name(lang)} (código: {lang})") + print(f"Confianza: {confidence:.2f}") + +if __name__ == "__main__": + main() +``` + +### 8.3 Servicios de LLM (Modelos de Lenguaje Grandes) + +El proyecto integra una fábrica de servicios (`LLMFactory`) para interactuar con diferentes Modelos de Lenguaje Grandes (LLMs). Esto permite a los scripts aprovechar la IA generativa para tareas como el análisis de código, la generación de descripciones semánticas, etc. + +#### 8.3.1 Configuración de API Keys + +La mayoría de los servicios de LLM requieren una clave de API para funcionar. El sistema gestiona esto de forma centralizada a través de variables de entorno. + +1. **Crear el archivo `.env`**: En el **directorio raíz del proyecto**, crea un archivo llamado `.env` (si aún no existe). + +2. **Añadir las API Keys**: Abre el archivo `.env` y añade las claves para los servicios que planeas utilizar. El sistema cargará estas variables automáticamente al inicio. + + ```env + # Ejemplo de contenido para el archivo .env + # (Solo necesitas añadir las claves de los servicios que vayas a usar) + + OPENAI_API_KEY="sk-..." + GROQ_API_KEY="gsk_..." + CLAUDE_API_KEY="sk-ant-..." + GEMINI_API_KEY="AIzaSy..." + GROK_API_KEY="TU_API_KEY_DE_GROK" + ``` + + **Nota**: El servicio `ollama` se ejecuta localmente y no requiere una clave de API. + +#### 8.3.2 Ejemplo de importación y uso + +El siguiente ejemplo muestra cómo un script puede cargar su configuración, inicializar un servicio de LLM y usarlo para generar texto. Este patrón es similar al utilizado en `x3_generate_semantic_descriptions.py`. + +```python +# Asegúrate de tener el path raíz configurado +# ... (código de configuración de sys.path) + +from services.llm.llm_factory import LLMFactory +from backend.script_utils import load_configuration + +def main(): + # Cargar la configuración del script, que puede incluir qué LLM usar + configs = load_configuration() + llm_configs = configs.get("llm", {}) + + # Obtener el tipo de servicio y otros parámetros del config + # Por defecto, usamos 'groq' si no se especifica + service_type = llm_configs.get("service", "groq") + + print(f"🤖 Inicializando servicio LLM: {service_type}") + + # Crear una instancia del servicio usando la fábrica + # La fábrica se encarga de pasar las API keys desde las variables de entorno + llm_service = LLMFactory.create_service(service_type, **llm_configs) + + if not llm_service: + print(f"❌ Error: No se pudo crear el servicio LLM '{service_type}'. Abortando.") + return + + # Usar el servicio para generar texto + try: + prompt = "Explica la computación cuántica en una sola frase." + print(f"Enviando prompt: '{prompt}'") + + description = llm_service.generate_text(prompt) + + print("\nRespuesta del LLM:") + print(description) + + except Exception as e: + print(f"Ocurrió un error al contactar al servicio LLM: {e}") + +if __name__ == "__main__": + main() +``` + +#### 8.3.3 Servicios Disponibles + +La `LLMFactory` soporta los siguientes tipos de servicio (`service_type`): +- `openai` +- `groq` +- `claude` +- `gemini` +- `grok` +- `ollama` (para ejecución local) \ No newline at end of file diff --git a/backend/script_groups/IO_adaptation/data.json b/backend/script_groups/IO_adaptation/data.json index 5fda70d..8edad2b 100644 --- a/backend/script_groups/IO_adaptation/data.json +++ b/backend/script_groups/IO_adaptation/data.json @@ -1,4 +1,4 @@ { - "ObsideanDir": "C:\\Users\\migue\\OneDrive\\Miguel\\Obsidean\\Trabajo\\VM", - "ObsideanProjectsBase": "\\04-SIDEL" + "aml_exp_directory": "aml", + "resultados_exp_directory": "Resultados" } \ No newline at end of file diff --git a/backend/script_groups/IO_adaptation/esquema_group.json b/backend/script_groups/IO_adaptation/esquema_group.json index 97febac..576ce98 100644 --- a/backend/script_groups/IO_adaptation/esquema_group.json +++ b/backend/script_groups/IO_adaptation/esquema_group.json @@ -1,18 +1,17 @@ { "type": "object", "properties": { - "ObsideanDir": { + "aml_exp_directory": { "type": "string", - "title": "Directorio de Vault de Obsidean", - "description": "Directorio de Vault de Obsidean", - "format": "directory", - "default": "C:\\Users\\migue\\OneDrive\\Miguel\\Obsidean\\Trabajo\\VM" + "title": "Directorio de exportación AML", + "description": "", + "default": "aml" }, - "ObsideanProjectsBase": { + "resultados_exp_directory": { "type": "string", - "title": "Subdirectorio", - "description": "Subdirectorio de los proyectos actuales en el Vault de Obsidean", - "default": "\\04-SIDEL" + "title": "Subdirectorio donde se colocan las tablas", + "description": "Subdirectorio donde se colocan las tablas", + "default": "Resultados" } } } \ No newline at end of file diff --git a/backend/script_groups/IO_adaptation/esquema_work.json b/backend/script_groups/IO_adaptation/esquema_work.json index 323a504..26f74e4 100644 --- a/backend/script_groups/IO_adaptation/esquema_work.json +++ b/backend/script_groups/IO_adaptation/esquema_work.json @@ -7,17 +7,30 @@ "description": "", "format": "directory" }, + "siemens_tia_project": { + "type": "string", + "title": "Proyecto Tia Portal", + "description": "Donde esta el archivo *.ap18 - *.ap19 - *.ap20", + "format": "directory" + }, "tags_exp_directory": { "type": "string", "title": "Directorio con el archivo Excel de Tags exportado de Tia", "description": "", "format": "directory" }, - "siemens_tia_project": { + "ObsideanDir": { "type": "string", - "title": "Proyecto Tia Portal", - "description": "Donde esta el archivo *.ap18 - *.ap19 - *.ap20", - "format": "directory" + "title": "Directorio de Vault de Obsidean", + "description": "Directorio de Vault de Obsidean", + "format": "directory", + "default": "C:\\Users\\migue\\OneDrive\\Miguel\\Obsidean\\Trabajo\\VM" + }, + "ObsideanProjectsBase": { + "type": "string", + "title": "Subdirectorio", + "description": "Subdirectorio de los proyectos actuales en el Vault de Obsidean", + "default": "\\04-SIDEL" } } } \ No newline at end of file diff --git a/backend/script_groups/IO_adaptation/script_config.json b/backend/script_groups/IO_adaptation/script_config.json index 21ffd76..13aa7b9 100644 --- a/backend/script_groups/IO_adaptation/script_config.json +++ b/backend/script_groups/IO_adaptation/script_config.json @@ -4,9 +4,15 @@ "model": "gpt-3.5-turbo" }, "level2": { - "ObsideanDir": "C:\\Users\\migue\\OneDrive\\Miguel\\Obsidean\\Trabajo\\VM", - "ObsideanProjectsBase": "\\04-SIDEL" + "aml_exp_directory": "aml", + "resultados_exp_directory": "Resultados" }, - "level3": {}, - "working_directory": "C:\\Trabajo\\SIDEL\\13 - E5.007560 - Modifica O&U - SAE235\\Reporte\\IOTags" + "level3": { + "ObsideanDir": "C:\\Users\\migue\\OneDrive\\Miguel\\Obsidean\\Trabajo\\VM", + "ObsideanProjectsBase": "\\04-SIDEL", + "siemens_exp_directory": "C:\\Trabajo\\SIDEL\\13 - E5.007560 - Modifica O&U - SAE235\\Reporte\\ExportTia", + "siemens_tia_project": "C:/Trabajo/SIDEL/13 - E5.007560 - Modifica O&U - SAE235/InLavoro/PLC/SSAE0235/_NEW/SAE235_v0.4", + "tags_exp_directory": "C:\\Trabajo\\SIDEL\\13 - E5.007560 - Modifica O&U - SAE235\\Reporte\\IOTags" + }, + "working_directory": "C:\\Trabajo\\SIDEL\\13 - E5.007560 - Modifica O&U - SAE235\\Reporte\\Analisis\\Siemens" } \ No newline at end of file diff --git a/backend/script_groups/IO_adaptation/x1_export_CAx.py b/backend/script_groups/IO_adaptation/x1_export_CAx.py index 9b803d2..cfd3410 100644 --- a/backend/script_groups/IO_adaptation/x1_export_CAx.py +++ b/backend/script_groups/IO_adaptation/x1_export_CAx.py @@ -2,8 +2,6 @@ export_CAx_from_tia : Script que exporta los datos CAx de un proyecto de TIA Portal y genera un resumen en Markdown. """ -import tkinter as tk -from tkinter import filedialog import os import sys import traceback @@ -18,11 +16,7 @@ from backend.script_utils import load_configuration # --- Configuration --- # Supported TIA Portal versions mapping (extension -> version) -SUPPORTED_TIA_VERSIONS = { - ".ap18": "18.0", - ".ap19": "19.0", - ".ap20": "20.0" -} +SUPPORTED_TIA_VERSIONS = {".ap18": "18.0", ".ap19": "19.0", ".ap20": "20.0"} # --- TIA Scripting Import Handling --- # (Same import handling as the previous script) @@ -45,65 +39,47 @@ except Exception as e: # --- Functions --- -def get_supported_filetypes(): - """Returns the supported file types for TIA Portal projects.""" - filetypes = [] - for ext, version in SUPPORTED_TIA_VERSIONS.items(): - version_major = version.split('.')[0] - filetypes.append((f"TIA Portal V{version_major} Projects", f"*{ext}")) - - # Add option to show all supported files - all_extensions = " ".join([f"*{ext}" for ext in SUPPORTED_TIA_VERSIONS.keys()]) - filetypes.insert(0, ("All TIA Portal Projects", all_extensions)) - - return filetypes +def find_project_file_in_dir(directory): + """Finds a TIA project file in the given directory.""" + project_files = [] + supported_extensions = SUPPORTED_TIA_VERSIONS.keys() + for ext in supported_extensions: + project_files.extend(list(Path(directory).glob(f"*{ext}"))) + + if len(project_files) == 1: + return project_files[0] + elif len(project_files) == 0: + print(f"ERROR: No TIA Portal project file found in {directory}.") + print(f"Supported extensions: {list(supported_extensions)}") + return None + else: + print(f"ERROR: Multiple TIA Portal project files found in {directory}:") + for f in project_files: + print(f" - {f}") + print("Please ensure only one project file exists in the directory.") + return None def detect_tia_version(project_file_path): """Detects TIA Portal version based on file extension.""" file_path = Path(project_file_path) file_extension = file_path.suffix.lower() - + if file_extension in SUPPORTED_TIA_VERSIONS: detected_version = SUPPORTED_TIA_VERSIONS[file_extension] - print(f"Detected TIA Portal version: {detected_version} (from extension {file_extension})") + print( + f"Detected TIA Portal version: {detected_version} (from extension {file_extension})" + ) return detected_version else: - print(f"WARNING: Unrecognized file extension '{file_extension}'. Supported extensions: {list(SUPPORTED_TIA_VERSIONS.keys())}") + print( + f"WARNING: Unrecognized file extension '{file_extension}'. Supported extensions: {list(SUPPORTED_TIA_VERSIONS.keys())}" + ) # Default to version 18.0 for backward compatibility print("Defaulting to TIA Portal V18.0") return "18.0" -def select_project_file(): - """Opens a dialog to select a TIA Portal project file.""" - root = tk.Tk() - root.withdraw() - file_path = filedialog.askopenfilename( - title="Select TIA Portal Project File", - filetypes=get_supported_filetypes(), - ) - root.destroy() - if not file_path: - print("No project file selected. Exiting.") - sys.exit(0) - return file_path - - -def select_output_directory(): - """Opens a dialog to select the output directory.""" - root = tk.Tk() - root.withdraw() - dir_path = filedialog.askdirectory( - title="Select Output Directory for AML and MD files" - ) - root.destroy() - if not dir_path: - print("No output directory selected. Exiting.") - sys.exit(0) - return dir_path - - def find_elements(element, path): """Helper to find elements using namespaces commonly found in AML.""" # AutomationML namespaces often vary slightly or might be default @@ -253,7 +229,14 @@ def parse_aml_to_markdown(aml_file_path, md_file_path): if __name__ == "__main__": configs = load_configuration() + + # Get parameters from configuration file + level3_configs = configs.get("level3", {}) + level2_configs = configs.get("level2", {}) + siemens_tia_project_dir = level3_configs.get("siemens_tia_project") working_directory = configs.get("working_directory") + aml_exp_directory = level2_configs.get("aml_exp_directory") + resultados_exp_directory = level2_configs.get("resultados_exp_directory") print("--- TIA Portal Project CAx Exporter and Analyzer ---") @@ -263,14 +246,33 @@ if __name__ == "__main__": print("Please configure the working directory using the main application.") sys.exit(1) - # 1. Select Project File, Output Directory comes from config - project_file = select_project_file() - output_dir = Path( - working_directory - ) # Use working directory from config, ensure it's a Path object + if not aml_exp_directory: + print("ERROR: aml_exp_directory not set in level2 configuration.") + sys.exit(1) + + if not resultados_exp_directory: + print("ERROR: resultados_exp_directory not set in level2 configuration.") + sys.exit(1) + + # Validate TIA project directory from config + if not siemens_tia_project_dir or not os.path.isdir(siemens_tia_project_dir): + print("ERROR: TIA project directory is not configured or invalid.") + print( + 'Please set the "siemens_tia_project" path in your configuration (work_dir.json).' + ) + sys.exit(1) + + # 1. Find Project File in the configured directory + project_file = find_project_file_in_dir(siemens_tia_project_dir) + if not project_file: + sys.exit(1) # Error message is printed inside the function + + aml_output_dir = Path(working_directory) / aml_exp_directory + md_output_dir = Path(working_directory) / resultados_exp_directory print(f"\nSelected Project: {project_file}") - print(f"Using Output Directory (Working Directory): {output_dir}") + print(f"Using AML Output Directory: {aml_output_dir}") + print(f"Using Results Output Directory: {md_output_dir}") # 2. Detect TIA Portal version from project file tia_version = detect_tia_version(project_file) @@ -278,10 +280,10 @@ if __name__ == "__main__": # Define output file names using Path object project_path = Path(project_file) project_base_name = project_path.stem # Get filename without extension - aml_file = output_dir / f"{project_base_name}_CAx_Export.aml" - md_file = output_dir / f"{project_base_name}_CAx_Summary.md" + aml_file = aml_output_dir / f"{project_base_name}_CAx_Export.aml" + md_file = md_output_dir / f"{project_base_name}_CAx_Summary.md" log_file = ( - output_dir / f"{project_base_name}_CAx_Export.log" + aml_output_dir / f"{project_base_name}_CAx_Export.log" ) # Log file for the export process print(f"Will export CAx data to: {aml_file}") @@ -317,8 +319,17 @@ if __name__ == "__main__": # 5. Export CAx Data (Project Level) print(f"Exporting CAx data for the project to {aml_file}...") - # Ensure output directory exists (Path.mkdir handles this implicitly if needed later, but good practice) - output_dir.mkdir(parents=True, exist_ok=True) + # Ensure output directories exist + aml_output_dir.mkdir(parents=True, exist_ok=True) + md_output_dir.mkdir(parents=True, exist_ok=True) + + # Delete existing files to allow overwrite by TIA Portal + if aml_file.exists(): + print(f"Deleting existing AML file to allow overwrite: {aml_file}") + aml_file.unlink() + if log_file.exists(): + print(f"Deleting existing log file to allow overwrite: {log_file}") + log_file.unlink() # Pass paths as strings to the TIA function export_result = project_object.export_cax_data( @@ -337,9 +348,6 @@ if __name__ == "__main__": f"# Error\n\nCAx data export failed. Check log file: {log_file}" ) - except ts.TiaException as tia_ex: - print(f"\nTIA Portal Openness Error: {tia_ex}") - traceback.print_exc() except FileNotFoundError: print(f"\nERROR: Project file not found at {project_file}") except Exception as e: diff --git a/backend/script_groups/IO_adaptation/x2_process_CAx.py b/backend/script_groups/IO_adaptation/x2_process_CAx.py index 9201261..3f8ec7a 100644 --- a/backend/script_groups/IO_adaptation/x2_process_CAx.py +++ b/backend/script_groups/IO_adaptation/x2_process_CAx.py @@ -43,14 +43,14 @@ def extract_aml_data(root): if not elem_id: continue device_info = { - "name": elem.get("Name", "N/A"), + "name": elem.get("Name", ""), "id": elem_id, - "class": "N/A", - "type_identifier": "N/A", - "order_number": "N/A", - "type_name": "N/A", - "firmware_version": "N/A", - "position": elem.get("PositionNumber", "N/A"), + "class": "", + "type_identifier": "", + "order_number": "", + "type_name": "", + "firmware_version": "", + "position": elem.get("PositionNumber", ""), "attributes": {}, "interfaces": [], "network_nodes": [], @@ -68,9 +68,9 @@ def extract_aml_data(root): } class_tag = elem.xpath("./*[local-name()='SystemUnitClass']") device_info["class"] = ( - class_tag[0].get("Path", elem.get("RefBaseSystemUnitPath", "N/A")) + class_tag[0].get("Path", elem.get("RefBaseSystemUnitPath", "")) if class_tag - else elem.get("RefBaseSystemUnitPath", "N/A") + else elem.get("RefBaseSystemUnitPath", "") ) attributes = elem.xpath("./*[local-name()='Attribute']") for attr in attributes: @@ -93,9 +93,9 @@ def extract_aml_data(root): for part in address_parts: addr_details = { "area": part.get("Name", "?"), - "start": "N/A", - "length": "N/A", - "type": "N/A", + "start": "", + "length": "", + "type": "", } start_val = part.xpath( "./*[local-name()='Attribute'][@Name='StartAddress']/*[local-name()='Value']/text()" @@ -112,15 +112,15 @@ def extract_aml_data(root): addr_details["length"] = len_val[0] if type_val: addr_details["type"] = type_val[0] - if addr_details["start"] != "N/A": + if addr_details["start"] != "": device_info["io_addresses"].append(addr_details) interfaces = elem.xpath("./*[local-name()='ExternalInterface']") for interface in interfaces: device_info["interfaces"].append( { - "name": interface.get("Name", "N/A"), - "id": interface.get("ID", "N/A"), - "ref_base_class": interface.get("RefBaseClassPath", "N/A"), + "name": interface.get("Name", ""), + "id": interface.get("ID", ""), + "ref_base_class": interface.get("RefBaseClassPath", ""), } ) network_node_elements = elem.xpath( @@ -137,8 +137,8 @@ def extract_aml_data(root): node_info = { "id": node_id, "name": node_elem.get("Name", device_info["name"]), - "type": "N/A", - "address": "N/A", + "type": "", + "address": "", } type_attr = node_elem.xpath( "./*[local-name()='Attribute'][@Name='Type']/*[local-name()='Value']/text()" @@ -150,23 +150,23 @@ def extract_aml_data(root): node_info["type"] = type_attr[0] if addr_attr: node_info["address"] = addr_attr[0] - if node_info["address"] == "N/A": + if not node_info["address"]: parent_addr_attr = elem.xpath( "./*[local-name()='Attribute'][@Name='NetworkAddress']/*[local-name()='Value']/text()" ) if parent_addr_attr: node_info["address"] = parent_addr_attr[0] - if node_info["type"] == "N/A": + if not node_info["type"]: parent_type_attr = elem.xpath( "./*[local-name()='Attribute'][@Name='Type']/*[local-name()='Value']/text()" ) if parent_type_attr: node_info["type"] = parent_type_attr[0] - if node_info["address"] != "N/A": + if node_info["address"]: len_attr = node_elem.xpath( "./*[local-name()='Attribute'][@Name='Length']/*[local-name()='Value']/text()" ) - node_info["length"] = len_attr[0] if len_attr else "N/A" + node_info["length"] = len_attr[0] if len_attr else "" device_info["network_nodes"].append(node_info) device_id_to_parent_device[node_id] = elem_id data["devices"][elem_id] = device_info @@ -184,7 +184,7 @@ def extract_aml_data(root): "6ES7 51", ] if any( - device.get("order_number", "N/A").startswith(prefix) + device.get("order_number", "").startswith(prefix) for prefix in plc_order_prefixes ): is_plc = True @@ -207,7 +207,7 @@ def extract_aml_data(root): if not is_child_of_plc: if dev_id not in plc_ids_found: print( - f" Identified PLC: {device['name']} ({dev_id}) - Type: {device.get('type_name', 'N/A')} OrderNo: {device.get('order_number', 'N/A')}" + f" Identified PLC: {device['name']} ({dev_id}) - Type: {device.get('type_name', '')} OrderNo: {device.get('order_number', '')}" ) device["connected_networks"] = {} data["plcs"][dev_id] = device @@ -231,7 +231,7 @@ def extract_aml_data(root): elif "PROFIBUS" in rc_upper: net_type = "Profibus" break - + # If still unknown, check the Type attribute (crucial for PROFINET) if net_type == "Unknown": type_attr = device.get("attributes", {}).get("Type", "") @@ -239,7 +239,7 @@ def extract_aml_data(root): net_type = "Ethernet/Profinet" elif type_attr.upper() == "PROFIBUS": net_type = "Profibus" - + # Finally, check device name as fallback if net_type == "Unknown": if "PROFIBUS" in device["name"].upper(): @@ -247,7 +247,8 @@ def extract_aml_data(root): elif ( "ETHERNET" in device["name"].upper() or "PROFINET" in device["name"].upper() - or "PN/IE" in device["name"].upper() # Add common PROFINET naming pattern + or "PN/IE" + in device["name"].upper() # Add common PROFINET naming pattern ): net_type = "Ethernet/Profinet" if is_network: @@ -272,13 +273,13 @@ def extract_aml_data(root): side_b_ref = link.get("RefPartnerSideB", "") side_a_match = re.match(r"([^:]+):?(.*)", side_a_ref) side_b_match = re.match(r"([^:]+):?(.*)", side_b_ref) - side_a_id = side_a_match.group(1) if side_a_match else "N/A" + side_a_id = side_a_match.group(1) if side_a_match else "" side_a_suffix = ( side_a_match.group(2) if side_a_match and side_a_match.group(2) else side_a_id ) - side_b_id = side_b_match.group(1) if side_b_match else "N/A" + side_b_id = side_b_match.group(1) if side_b_match else "" side_b_suffix = ( side_b_match.group(2) if side_b_match and side_b_match.group(2) @@ -303,19 +304,19 @@ def extract_aml_data(root): device_info = data["devices"].get(linked_device_id) if not device_info: continue - address = "N/A" + address = "" node_info_for_addr = data["devices"].get(device_node_id, {}) for node in node_info_for_addr.get("network_nodes", []): if node.get("id") == device_node_id: - address = node.get("address", "N/A") + address = node.get("address", "") break - if address == "N/A": + if not address: address = node_info_for_addr.get("attributes", {}).get( - "NetworkAddress", "N/A" + "NetworkAddress", "" ) - if address == "N/A": + if not address: address = device_info.get("attributes", {}).get( - "NetworkAddress", "N/A" + "NetworkAddress", "" ) node_name_for_log = node_info_for_addr.get("name", device_node_id) print( @@ -347,7 +348,7 @@ def extract_aml_data(root): current_search_id = linked_device_id search_depth = 0 max_search_depth = 10 - + while current_search_id and search_depth < max_search_depth: # Check current device and all its children for PLCs device_to_check = data["devices"].get(current_search_id) @@ -356,12 +357,14 @@ def extract_aml_data(root): for child_id in device_to_check.get("children_ids", []): if child_id in data["plcs"]: potential_plc_id = child_id - print(f" --> Found PLC in children: {data['plcs'][child_id].get('name', 'Unknown PLC')} (ID: {child_id})") + print( + f" --> Found PLC in children: {data['plcs'][child_id].get('name', 'Unknown PLC')} (ID: {child_id})" + ) break - + if potential_plc_id: break - + # Move up to parent current_search_id = device_to_check.get("parent_id") search_depth += 1 @@ -379,10 +382,7 @@ def extract_aml_data(root): data["plcs"][potential_plc_id]["connected_networks"][ network_id ] = address - elif ( - plc_object["connected_networks"][network_id] == "N/A" - and address != "N/A" - ): + elif not plc_object["connected_networks"][network_id] and address: print( f" --> Updating address for Network '{data['networks'][network_id]['name']}' on PLC '{plc_object.get('name', 'Unknown PLC')}' to: {address}" ) @@ -410,7 +410,7 @@ def extract_aml_data(root): side_a_id, side_a_suffix, ) - if source_id != "N/A" and target_id != "N/A": + if source_id and target_id: if source_id not in data["links_by_source"]: data["links_by_source"][source_id] = [] if target_id not in data["links_by_target"]: @@ -450,14 +450,16 @@ def find_io_recursively(device_id, project_data, module_context): if device_info.get("io_addresses"): # Slot position is from the current device_info (which holds the IO) # It's often found in attributes.PositionNumber for sub-elements. - slot_pos = device_info.get("attributes", {}).get("PositionNumber", device_info.get("position", "N/A")) + slot_pos = device_info.get("attributes", {}).get( + "PositionNumber", device_info.get("position", "") + ) for addr in device_info["io_addresses"]: io_list.append( { "module_id": module_context["id"], "module_name": module_context["name"], - "module_pos": slot_pos, # Slot of the IO sub-element + "module_pos": slot_pos, # Slot of the IO sub-element "module_order_number": module_context["order_number"], "module_type_name": module_context["type_name"], **addr, @@ -473,32 +475,44 @@ def find_io_recursively(device_id, project_data, module_context): # --- generate_io_summary_file function (Updated) --- -def generate_io_summary_file(all_plc_io_for_table, md_file_path, plc_name, project_data, output_root_path): +def generate_io_summary_file( + all_plc_io_for_table, md_file_path, plc_name, project_data, output_root_path +): """ Generates a Hardware.md file with the IO summary table. If there's only one PLC, creates it in the root directory, otherwise creates PLC-specific named files. """ - + # Determine if this is the only PLC in the project plcs_count = len(project_data.get("plcs", {})) is_single_plc = plcs_count == 1 - + if is_single_plc: # For single PLC: create Hardware.md in the root directory hardware_file_path = os.path.join(output_root_path, "Hardware.md") file_title = f"# IO Summary Table for PLC: {plc_name}" else: # For multiple PLCs: create [PLC_Name]_Hardware.md in PLC's directory - hardware_file_path = os.path.join(os.path.dirname(md_file_path), f"{sanitize_filename(plc_name)}_Hardware.md") + hardware_file_path = os.path.join( + os.path.dirname(md_file_path), f"{sanitize_filename(plc_name)}_Hardware.md" + ) file_title = f"# IO Summary Table for PLC: {plc_name}" - + markdown_lines = [file_title, ""] - + if all_plc_io_for_table: # Define table headers headers = [ - "Network", "Type", "Address", "Device Name", "Sub-Device", - "OrderNo", "Type", "IO Type", "IO Address", "Number of Bits" + "Network", + "Type", + "Address", + "Device Name", + "Sub-Device", + "OrderNo", + "Type", + "IO Type", + "IO Address", + "Number of Bits", ] markdown_lines.append("| " + " | ".join(headers) + " |") markdown_lines.append("|-" + "-|-".join(["---"] * len(headers)) + "-|") @@ -509,23 +523,23 @@ def generate_io_summary_file(all_plc_io_for_table, md_file_path, plc_name, proje # Add rows to the table for row_data in sorted_table_data: row = [ - row_data.get("Network", "N/A"), - row_data.get("Network Type", "N/A"), - row_data.get("Device Address", "N/A"), - row_data.get("Device Name", "N/A"), - row_data.get("Sub-Device", "N/A"), - row_data.get("Sub-Device OrderNo", "N/A"), - row_data.get("Sub-Device Type", "N/A"), - row_data.get("IO Type", "N/A"), - f"`{row_data.get('IO Address', 'N/A')}`", # Format IO Address as code - row_data.get("Number of Bits", "N/A"), + row_data.get("Network", ""), + row_data.get("Network Type", ""), + row_data.get("Device Address", ""), + row_data.get("Device Name", ""), + row_data.get("Sub-Device", ""), + row_data.get("Sub-Device OrderNo", ""), + row_data.get("Sub-Device Type", ""), + row_data.get("IO Type", ""), + f"`{row_data.get('IO Address', '')}`", # Format IO Address as code + row_data.get("Number of Bits", ""), ] # Escape pipe characters within cell content if necessary - row = [str(cell).replace('|', '\\|') for cell in row] + row = [str(cell).replace("|", "\\|") for cell in row] markdown_lines.append("| " + " | ".join(row) + " |") else: markdown_lines.append("*No IO data found for this PLC.*") - + try: with open(hardware_file_path, "w", encoding="utf-8") as f: f.write("\n".join(markdown_lines)) @@ -533,27 +547,29 @@ def generate_io_summary_file(all_plc_io_for_table, md_file_path, plc_name, proje except Exception as e: print(f"ERROR writing Hardware.md file {hardware_file_path}: {e}") traceback.print_exc() - + return hardware_file_path # --- generate_io_excel_report function --- -def generate_io_excel_report(project_data, excel_file_path, target_plc_id, output_root_path): +def generate_io_excel_report( + project_data, excel_file_path, target_plc_id, output_root_path +): """ Genera un archivo Excel con información detallada de IOs por nodos del PLC. """ - + plc_info = project_data.get("plcs", {}).get(target_plc_id) if not plc_info: print(f"PLC ID '{target_plc_id}' not found in project data.") return - - plc_name = plc_info.get('name', target_plc_id) + + plc_name = plc_info.get("name", target_plc_id) print(f"Generating Excel IO report for PLC: {plc_name}") - + # Lista para almacenar todas las filas del Excel excel_rows = [] - + # v32.5: First, process PLC's local modules (modules in the same rack/structure as the PLC) plc_parent_id = plc_info.get("parent_id") if plc_parent_id: @@ -564,8 +580,8 @@ def generate_io_excel_report(project_data, excel_file_path, target_plc_id, outpu module_context = { "id": dev_id, "name": dev_info.get("name", dev_id), - "order_number": dev_info.get("order_number", "N/A"), - "type_name": dev_info.get("type_name", "N/A") + "order_number": dev_info.get("order_number", ""), + "type_name": dev_info.get("type_name", ""), } module_ios = find_io_recursively(dev_id, project_data, module_context) if module_ios: @@ -575,33 +591,33 @@ def generate_io_excel_report(project_data, excel_file_path, target_plc_id, outpu module_id = addr_info.get("module_id") if module_id not in ios_by_module: ios_by_module[module_id] = { - 'module_info': { - 'name': addr_info.get('module_name', '?'), - 'type': addr_info.get('module_type_name', 'N/A'), - 'order': addr_info.get('module_order_number', 'N/A'), - 'position': addr_info.get('module_pos', 'N/A') + "module_info": { + "name": addr_info.get("module_name", "?"), + "type": addr_info.get("module_type_name", ""), + "order": addr_info.get("module_order_number", ""), + "position": addr_info.get("module_pos", ""), }, - 'inputs': [], - 'outputs': [] + "inputs": [], + "outputs": [], } - + # Clasificar IO como input u output io_type = addr_info.get("type", "").lower() if io_type == "input": - ios_by_module[module_id]['inputs'].append(addr_info) + ios_by_module[module_id]["inputs"].append(addr_info) elif io_type == "output": - ios_by_module[module_id]['outputs'].append(addr_info) - + ios_by_module[module_id]["outputs"].append(addr_info) + # Crear una fila por cada módulo local con IOs for module_id, module_data in ios_by_module.items(): - module_info = module_data['module_info'] - + module_info = module_data["module_info"] + # Calcular direcciones de entrada - Start + Word Count - input_start_addr = 'N/A' + input_start_addr = "" input_word_count = 0 total_input_bits = 0 - - for addr_info in module_data['inputs']: + + for addr_info in module_data["inputs"]: start_str = addr_info.get("start", "?") length_str = addr_info.get("length", "?") try: @@ -610,13 +626,13 @@ def generate_io_excel_report(project_data, excel_file_path, target_plc_id, outpu length_bytes = math.ceil(length_bits / 8.0) if length_bits > 0 and length_bytes == 0: length_bytes = 1 - + # Para múltiples rangos, tomar el primer inicio y sumar words - if input_start_addr == 'N/A': + if not input_start_addr: input_start_addr = start_byte else: input_start_addr = min(input_start_addr, start_byte) - + # Convertir bytes a words (asumiendo words de 2 bytes) word_count = math.ceil(length_bytes / 2.0) input_word_count += word_count @@ -624,13 +640,13 @@ def generate_io_excel_report(project_data, excel_file_path, target_plc_id, outpu except: # En caso de error, mantener N/A pass - + # Calcular direcciones de salida - Start + Word Count - output_start_addr = 'N/A' + output_start_addr = "" output_word_count = 0 total_output_bits = 0 - - for addr_info in module_data['outputs']: + + for addr_info in module_data["outputs"]: start_str = addr_info.get("start", "?") length_str = addr_info.get("length", "?") try: @@ -639,13 +655,15 @@ def generate_io_excel_report(project_data, excel_file_path, target_plc_id, outpu length_bytes = math.ceil(length_bits / 8.0) if length_bits > 0 and length_bytes == 0: length_bytes = 1 - + # Para múltiples rangos, tomar el primer inicio y sumar words - if output_start_addr == 'N/A': + if not output_start_addr: output_start_addr = start_byte else: - output_start_addr = min(output_start_addr, start_byte) - + output_start_addr = min( + output_start_addr, start_byte + ) + # Convertir bytes a words (asumiendo words de 2 bytes) word_count = math.ceil(length_bytes / 2.0) output_word_count += word_count @@ -653,166 +671,204 @@ def generate_io_excel_report(project_data, excel_file_path, target_plc_id, outpu except: # En caso de error, mantener N/A pass - - excel_rows.append({ - 'PLC Name': plc_name, - 'Network Path': f"Local I/O -> {module_info['name']}", - 'Network Type': 'Local I/O', - 'Device Address': 'Local', - 'Device Name': f"PLC {plc_name}", - 'Device Type': module_info['type'], - 'Order Number': module_info['order'], - 'Firmware Version': 'N/A', - 'Position': module_info['position'], - 'IO Input Start Address': input_start_addr, - 'IO Input Word Count': input_word_count if input_word_count > 0 else 'N/A', - 'IO Output Start Address': output_start_addr, - 'IO Output Word Count': output_word_count if output_word_count > 0 else 'N/A', - 'Total Input Bits': total_input_bits, - 'Total Output Bits': total_output_bits, - 'Module Name': module_info['name'], - 'Module Type': module_info['type'], - 'Module Order Number': module_info['order'] - }) + + excel_rows.append( + { + "PLC Name": plc_name, + "Network Path": f"Local I/O -> {module_info['name']}", + "Network Type": "Local I/O", + "Device Address": "Local", + "Device Name": f"PLC {plc_name}", + "Device Type": module_info["type"], + "Order Number": module_info["order"], + "Firmware Version": "", + "Position": module_info["position"], + "IO Input Start Address": input_start_addr, + "IO Input Word Count": ( + input_word_count if input_word_count > 0 else "" + ), + "IO Output Start Address": output_start_addr, + "IO Output Word Count": ( + output_word_count if output_word_count > 0 else "" + ), + "Total Input Bits": total_input_bits, + "Total Output Bits": total_output_bits, + "Module Name": module_info["name"], + "Module Type": module_info["type"], + "Module Order Number": module_info["order"], + } + ) # Procesar las redes conectadas al PLC plc_networks = plc_info.get("connected_networks", {}) - + if not plc_networks and not excel_rows: # Si no hay redes, crear una fila básica del PLC - excel_rows.append({ - 'PLC Name': plc_name, - 'Network Path': 'No networks connected', - 'Network Type': 'N/A', - 'Device Address': 'N/A', - 'Device Name': plc_name, - 'Device Type': plc_info.get("type_name", "N/A"), - 'Order Number': plc_info.get("order_number", "N/A"), - 'Firmware Version': plc_info.get("firmware_version", "N/A"), - 'Position': plc_info.get("position", "N/A"), - 'IO Input Start Address': 'N/A', - 'IO Input Word Count': 'N/A', - 'IO Output Start Address': 'N/A', - 'IO Output Word Count': 'N/A', - 'Total Input Bits': 0, - 'Total Output Bits': 0, - 'Module Name': 'N/A', - 'Module Type': 'N/A', - 'Module Order Number': 'N/A' - }) + excel_rows.append( + { + "PLC Name": plc_name, + "Network Path": "No networks connected", + "Network Type": "", + "Device Address": "", + "Device Name": plc_name, + "Device Type": plc_info.get("type_name", ""), + "Order Number": plc_info.get("order_number", ""), + "Firmware Version": plc_info.get("firmware_version", ""), + "Position": plc_info.get("position", ""), + "IO Input Start Address": "", + "IO Input Word Count": "", + "IO Output Start Address": "", + "IO Output Word Count": "", + "Total Input Bits": 0, + "Total Output Bits": 0, + "Module Name": "", + "Module Type": "", + "Module Order Number": "", + } + ) else: # Procesar cada red conectada for net_id, plc_addr_on_net in plc_networks.items(): net_info = project_data.get("networks", {}).get(net_id) if not net_info: continue - - network_name = net_info.get('name', net_id) - network_type = net_info.get('type', 'Unknown') + + network_name = net_info.get("name", net_id) + network_type = net_info.get("type", "Unknown") devices_on_net = net_info.get("devices_on_net", {}) - + # Identificar nodos que pertenecen al PLC para excluirlos de la lista de dispositivos plc_interface_and_node_ids = set() for node in plc_info.get("network_nodes", []): plc_interface_and_node_ids.add(node["id"]) - interface_id_lookup = project_data["devices"].get(node["id"], {}).get("parent_id") + interface_id_lookup = ( + project_data["devices"].get(node["id"], {}).get("parent_id") + ) if interface_id_lookup: plc_interface_and_node_ids.add(interface_id_lookup) plc_interface_and_node_ids.add(target_plc_id) - + # Filtrar dispositivos que no son interfaces del PLC other_devices = [ - (node_id, node_addr) + (node_id, node_addr) for node_id, node_addr in devices_on_net.items() if node_id not in plc_interface_and_node_ids ] - + if not other_devices: # Si no hay otros dispositivos, crear fila solo para el PLC en esta red - excel_rows.append({ - 'PLC Name': plc_name, - 'Network Path': f"{network_name} -> {plc_name}", - 'Network Type': network_type, - 'Device Address': plc_addr_on_net, - 'Device Name': plc_name, - 'Device Type': plc_info.get("type_name", "N/A"), - 'Order Number': plc_info.get("order_number", "N/A"), - 'Firmware Version': plc_info.get("firmware_version", "N/A"), - 'Position': plc_info.get("position", "N/A"), - 'IO Input Start Address': 'N/A', - 'IO Input Word Count': 'N/A', - 'IO Output Start Address': 'N/A', - 'IO Output Word Count': 'N/A', - 'Total Input Bits': 0, - 'Total Output Bits': 0, - 'Module Name': 'PLC Main Unit', - 'Module Type': plc_info.get("type_name", "N/A"), - 'Module Order Number': plc_info.get("order_number", "N/A") - }) + excel_rows.append( + { + "PLC Name": plc_name, + "Network Path": f"{network_name} -> {plc_name}", + "Network Type": network_type, + "Device Address": plc_addr_on_net, + "Device Name": plc_name, + "Device Type": plc_info.get("type_name", ""), + "Order Number": plc_info.get("order_number", ""), + "Firmware Version": plc_info.get("firmware_version", ""), + "Position": plc_info.get("position", ""), + "IO Input Start Address": "", + "IO Input Word Count": "", + "IO Output Start Address": "", + "IO Output Word Count": "", + "Total Input Bits": 0, + "Total Output Bits": 0, + "Module Name": "PLC Main Unit", + "Module Type": plc_info.get("type_name", ""), + "Module Order Number": plc_info.get("order_number", ""), + } + ) else: # Procesar cada dispositivo en la red for node_id, node_addr in other_devices: node_info = project_data.get("devices", {}).get(node_id) if not node_info: continue - + # Determinar la estructura jerárquica del dispositivo interface_id = node_info.get("parent_id") interface_info = None actual_device_id = None actual_device_info = None - + if interface_id: - interface_info = project_data.get("devices", {}).get(interface_id) + interface_info = project_data.get("devices", {}).get( + interface_id + ) if interface_info: actual_device_id = interface_info.get("parent_id") if actual_device_id: - actual_device_info = project_data.get("devices", {}).get(actual_device_id) - + actual_device_info = project_data.get( + "devices", {} + ).get(actual_device_id) + # Determinar qué información mostrar - display_info = actual_device_info if actual_device_info else (interface_info if interface_info else node_info) - display_id = actual_device_id if actual_device_info else (interface_id if interface_info else node_id) - + display_info = ( + actual_device_info + if actual_device_info + else (interface_info if interface_info else node_info) + ) + display_id = ( + actual_device_id + if actual_device_info + else (interface_id if interface_info else node_id) + ) + device_name = display_info.get("name", display_id) - device_type = display_info.get("type_name", "N/A") - device_order = display_info.get("order_number", "N/A") - device_position = display_info.get("position", "N/A") - firmware_version = display_info.get("firmware_version", "N/A") - + device_type = display_info.get("type_name", "") + device_order = display_info.get("order_number", "") + device_position = display_info.get("position", "") + firmware_version = display_info.get("firmware_version", "") + # Construir el path de red network_path = f"{network_name} ({network_type}) -> {device_name} @ {node_addr}" - + # Buscar IOs recursivamente io_search_root_id = display_id - io_search_root_info = project_data.get("devices", {}).get(io_search_root_id) - + io_search_root_info = project_data.get("devices", {}).get( + io_search_root_id + ) + aggregated_io_addresses = [] - + # Buscar IOs en la estructura padre si existe - parent_structure_id = io_search_root_info.get("parent_id") if io_search_root_info else None - + parent_structure_id = ( + io_search_root_info.get("parent_id") + if io_search_root_info + else None + ) + if parent_structure_id: # Buscar IOs en dispositivos hermanos bajo la misma estructura padre - for dev_scan_id, dev_scan_info in project_data.get("devices", {}).items(): + for dev_scan_id, dev_scan_info in project_data.get( + "devices", {} + ).items(): if dev_scan_info.get("parent_id") == parent_structure_id: module_context = { "id": dev_scan_id, "name": dev_scan_info.get("name", dev_scan_id), - "order_number": dev_scan_info.get("order_number", "N/A"), - "type_name": dev_scan_info.get("type_name", "N/A") + "order_number": dev_scan_info.get( + "order_number", "" + ), + "type_name": dev_scan_info.get("type_name", ""), } - io_from_sibling = find_io_recursively(dev_scan_id, project_data, module_context) + io_from_sibling = find_io_recursively( + dev_scan_id, project_data, module_context + ) aggregated_io_addresses.extend(io_from_sibling) elif io_search_root_id: # Buscar IOs directamente en el dispositivo module_context = { "id": io_search_root_id, "name": io_search_root_info.get("name", io_search_root_id), - "order_number": io_search_root_info.get("order_number", "N/A"), - "type_name": io_search_root_info.get("type_name", "N/A") + "order_number": io_search_root_info.get("order_number", ""), + "type_name": io_search_root_info.get("type_name", ""), } - aggregated_io_addresses = find_io_recursively(io_search_root_id, project_data, module_context) - + aggregated_io_addresses = find_io_recursively( + io_search_root_id, project_data, module_context + ) + # Procesar IOs por módulo if aggregated_io_addresses: # Agrupar IOs por módulo @@ -821,33 +877,35 @@ def generate_io_excel_report(project_data, excel_file_path, target_plc_id, outpu module_id = addr_info.get("module_id") if module_id not in ios_by_module: ios_by_module[module_id] = { - 'module_info': { - 'name': addr_info.get('module_name', '?'), - 'type': addr_info.get('module_type_name', 'N/A'), - 'order': addr_info.get('module_order_number', 'N/A'), - 'position': addr_info.get('module_pos', 'N/A') + "module_info": { + "name": addr_info.get("module_name", "?"), + "type": addr_info.get("module_type_name", ""), + "order": addr_info.get( + "module_order_number", "" + ), + "position": addr_info.get("module_pos", ""), }, - 'inputs': [], - 'outputs': [] + "inputs": [], + "outputs": [], } - + # Clasificar IO como input u output io_type = addr_info.get("type", "").lower() if io_type == "input": - ios_by_module[module_id]['inputs'].append(addr_info) + ios_by_module[module_id]["inputs"].append(addr_info) elif io_type == "output": - ios_by_module[module_id]['outputs'].append(addr_info) - + ios_by_module[module_id]["outputs"].append(addr_info) + # Crear una fila por cada módulo con IOs for module_id, module_data in ios_by_module.items(): - module_info = module_data['module_info'] - + module_info = module_data["module_info"] + # Calcular direcciones de entrada - Start + Word Count - input_start_addr = 'N/A' + input_start_addr = "" input_word_count = 0 total_input_bits = 0 - - for addr_info in module_data['inputs']: + + for addr_info in module_data["inputs"]: start_str = addr_info.get("start", "?") length_str = addr_info.get("length", "?") try: @@ -856,13 +914,15 @@ def generate_io_excel_report(project_data, excel_file_path, target_plc_id, outpu length_bytes = math.ceil(length_bits / 8.0) if length_bits > 0 and length_bytes == 0: length_bytes = 1 - + # Para múltiples rangos, tomar el primer inicio y sumar words - if input_start_addr == 'N/A': + if not input_start_addr: input_start_addr = start_byte else: - input_start_addr = min(input_start_addr, start_byte) - + input_start_addr = min( + input_start_addr, start_byte + ) + # Convertir bytes a words (asumiendo words de 2 bytes) word_count = math.ceil(length_bytes / 2.0) input_word_count += word_count @@ -870,13 +930,13 @@ def generate_io_excel_report(project_data, excel_file_path, target_plc_id, outpu except: # En caso de error, mantener N/A pass - + # Calcular direcciones de salida - Start + Word Count - output_start_addr = 'N/A' + output_start_addr = "" output_word_count = 0 total_output_bits = 0 - - for addr_info in module_data['outputs']: + + for addr_info in module_data["outputs"]: start_str = addr_info.get("start", "?") length_str = addr_info.get("length", "?") try: @@ -885,13 +945,15 @@ def generate_io_excel_report(project_data, excel_file_path, target_plc_id, outpu length_bytes = math.ceil(length_bits / 8.0) if length_bits > 0 and length_bytes == 0: length_bytes = 1 - + # Para múltiples rangos, tomar el primer inicio y sumar words - if output_start_addr == 'N/A': + if not output_start_addr: output_start_addr = start_byte else: - output_start_addr = min(output_start_addr, start_byte) - + output_start_addr = min( + output_start_addr, start_byte + ) + # Convertir bytes a words (asumiendo words de 2 bytes) word_count = math.ceil(length_bytes / 2.0) output_word_count += word_count @@ -899,86 +961,117 @@ def generate_io_excel_report(project_data, excel_file_path, target_plc_id, outpu except: # En caso de error, mantener N/A pass - - excel_rows.append({ - 'PLC Name': plc_name, - 'Network Path': network_path, - 'Network Type': network_type, - 'Device Address': node_addr, - 'Device Name': device_name, - 'Device Type': device_type, - 'Order Number': device_order, - 'Firmware Version': firmware_version, - 'Position': device_position, - 'IO Input Start Address': input_start_addr, - 'IO Input Word Count': input_word_count if input_word_count > 0 else 'N/A', - 'IO Output Start Address': output_start_addr, - 'IO Output Word Count': output_word_count if output_word_count > 0 else 'N/A', - 'Total Input Bits': total_input_bits, - 'Total Output Bits': total_output_bits, - 'Module Name': module_info['name'], - 'Module Type': module_info['type'], - 'Module Order Number': module_info['order'] - }) + + excel_rows.append( + { + "PLC Name": plc_name, + "Network Path": network_path, + "Network Type": network_type, + "Device Address": node_addr, + "Device Name": device_name, + "Device Type": device_type, + "Order Number": device_order, + "Firmware Version": firmware_version, + "Position": device_position, + "IO Input Start Address": input_start_addr, + "IO Input Word Count": ( + input_word_count if input_word_count > 0 else "" + ), + "IO Output Start Address": output_start_addr, + "IO Output Word Count": ( + output_word_count + if output_word_count > 0 + else "" + ), + "Total Input Bits": total_input_bits, + "Total Output Bits": total_output_bits, + "Module Name": module_info["name"], + "Module Type": module_info["type"], + "Module Order Number": module_info["order"], + } + ) else: # Dispositivo sin IOs - excel_rows.append({ - 'PLC Name': plc_name, - 'Network Path': network_path, - 'Network Type': network_type, - 'Device Address': node_addr, - 'Device Name': device_name, - 'Device Type': device_type, - 'Order Number': device_order, - 'Firmware Version': firmware_version, - 'Position': device_position, - 'IO Input Start Address': 'N/A', - 'IO Input Word Count': 'N/A', - 'IO Output Start Address': 'N/A', - 'IO Output Word Count': 'N/A', - 'Total Input Bits': 0, - 'Total Output Bits': 0, - 'Module Name': 'N/A', - 'Module Type': 'N/A', - 'Module Order Number': 'N/A' - }) - + excel_rows.append( + { + "PLC Name": plc_name, + "Network Path": network_path, + "Network Type": network_type, + "Device Address": node_addr, + "Device Name": device_name, + "Device Type": device_type, + "Order Number": device_order, + "Firmware Version": firmware_version, + "Position": device_position, + "IO Input Start Address": "", + "IO Input Word Count": "", + "IO Output Start Address": "", + "IO Output Word Count": "", + "Total Input Bits": 0, + "Total Output Bits": 0, + "Module Name": "", + "Module Type": "", + "Module Order Number": "", + } + ) + # Crear DataFrame y guardar Excel if excel_rows: df = pd.DataFrame(excel_rows) - + # Agregar columna de ID único para compatibilidad con x7_update_CAx - df['Unique_ID'] = df['PLC Name'] + "+" + df['Device Name'] - + df["Unique_ID"] = df["PLC Name"] + "+" + df["Device Name"] + # Reordenar columnas para mejor legibilidad column_order = [ - 'PLC Name', 'Network Path', 'Network Type', 'Device Address', 'Device Name', - 'Device Type', 'Order Number', 'Firmware Version', 'Position', - 'Module Name', 'Module Type', 'Module Order Number', - 'IO Input Start Address', 'IO Input Word Count', 'IO Output Start Address', 'IO Output Word Count', - 'Total Input Bits', 'Total Output Bits', 'Unique_ID' # Agregar al final para compatibilidad + "PLC Name", + "Network Path", + "Network Type", + "Device Address", + "Device Name", + "Device Type", + "Order Number", + "Firmware Version", + "Position", + "Module Name", + "Module Type", + "Module Order Number", + "IO Input Start Address", + "IO Input Word Count", + "IO Output Start Address", + "IO Output Word Count", + "Total Input Bits", + "Total Output Bits", + "Unique_ID", # Agregar al final para compatibilidad ] df = df.reindex(columns=column_order) - + try: # Convertir columnas numéricas específicas a int para evitar decimales numeric_columns = [ - 'IO Input Start Address', 'IO Input Word Count', - 'IO Output Start Address', 'IO Output Word Count', - 'Total Input Bits', 'Total Output Bits' + "IO Input Start Address", + "IO Input Word Count", + "IO Output Start Address", + "IO Output Word Count", + "Total Input Bits", + "Total Output Bits", ] - + for col in numeric_columns: if col in df.columns: # Convertir a int manteniendo N/A como string - df[col] = df[col].apply(lambda x: int(x) if isinstance(x, (int, float)) and pd.notna(x) and x != 'N/A' else x) - + df[col] = df[col].apply( + lambda x: ( + int(x) if isinstance(x, (int, float)) and pd.notna(x) else x + ) + ) + # Guardar como Excel con formato - with pd.ExcelWriter(excel_file_path, engine='openpyxl') as writer: - df.to_excel(writer, sheet_name='IO Report', index=False) - + with pd.ExcelWriter(excel_file_path, engine="openpyxl") as writer: + df.to_excel(writer, sheet_name="IO Report", index=False) + # Ajustar ancho de columnas - worksheet = writer.sheets['IO Report'] + worksheet = writer.sheets["IO Report"] for column in worksheet.columns: max_length = 0 column_letter = column[0].column_letter @@ -990,10 +1083,10 @@ def generate_io_excel_report(project_data, excel_file_path, target_plc_id, outpu pass adjusted_width = min(max_length + 2, 50) # Máximo 50 caracteres worksheet.column_dimensions[column_letter].width = adjusted_width - + print(f"Excel IO report saved to: {excel_file_path}") print(f"Total rows in report: {len(excel_rows)}") - + except Exception as e: print(f"ERROR saving Excel file {excel_file_path}: {e}") traceback.print_exc() @@ -1004,11 +1097,11 @@ def generate_io_excel_report(project_data, excel_file_path, target_plc_id, outpu # --- generate_markdown_tree function --- def generate_markdown_tree(project_data, md_file_path, target_plc_id, output_root_path): """(Modified) Generates hierarchical Markdown for a specific PLC.""" - + plc_info = project_data.get("plcs", {}).get(target_plc_id) plc_name_for_title = "Unknown PLC" if plc_info: - plc_name_for_title = plc_info.get('name', target_plc_id) + plc_name_for_title = plc_info.get("name", target_plc_id) # v31: Initialize list to store all IO data for the summary table for this PLC all_plc_io_for_table = [] @@ -1016,7 +1109,9 @@ def generate_markdown_tree(project_data, md_file_path, target_plc_id, output_roo markdown_lines = [f"# Hardware & IO Summary for PLC: {plc_name_for_title}", ""] if not plc_info: - markdown_lines.append(f"*Details for PLC ID '{target_plc_id}' not found in the project data.*") + markdown_lines.append( + f"*Details for PLC ID '{target_plc_id}' not found in the project data.*" + ) try: with open(md_file_path, "w", encoding="utf-8") as f: f.write("\n".join(markdown_lines)) @@ -1027,14 +1122,14 @@ def generate_markdown_tree(project_data, md_file_path, target_plc_id, output_roo # Content previously in the loop now directly uses plc_info and target_plc_id markdown_lines.append(f"\n## PLC: {plc_info.get('name', target_plc_id)}") - type_name = plc_info.get("type_name", "N/A") - order_num = plc_info.get("order_number", "N/A") - firmware = plc_info.get("firmware_version", "N/A") - if type_name and type_name != "N/A": + type_name = plc_info.get("type_name", "") + order_num = plc_info.get("order_number", "") + firmware = plc_info.get("firmware_version", "") + if type_name and type_name != "": markdown_lines.append(f"- **Type Name:** `{type_name}`") - if order_num and order_num != "N/A": + if order_num and order_num != "": markdown_lines.append(f"- **Order Number:** `{order_num}`") - if firmware and firmware != "N/A": + if firmware and firmware != "": markdown_lines.append(f"- **Firmware:** `{firmware}`") # v32.5: Process PLC's local modules (modules in the same rack/structure as the PLC) @@ -1048,8 +1143,8 @@ def generate_markdown_tree(project_data, md_file_path, target_plc_id, output_roo module_context = { "id": dev_id, "name": dev_info.get("name", dev_id), - "order_number": dev_info.get("order_number", "N/A"), - "type_name": dev_info.get("type_name", "N/A") + "order_number": dev_info.get("order_number", ""), + "type_name": dev_info.get("type_name", ""), } module_ios = find_io_recursively(dev_id, project_data, module_context) if module_ios: @@ -1074,35 +1169,43 @@ def generate_markdown_tree(project_data, md_file_path, target_plc_id, output_roo except Exception: siemens_addr = f"FMT_ERROR({addr_info.get('start', '?')},{addr_info.get('length', '?')})" - plc_local_modules_io.append({ - "Network": "PLC Local Modules", - "Network Type": "Local I/O", - "Device Address": "Local", - "Device Name": f"PLC {plc_info.get('name', target_plc_id)}", - "Sub-Device": addr_info.get('module_name', '?'), - "Sub-Device OrderNo": addr_info.get('module_order_number', 'N/A'), - "Sub-Device Type": addr_info.get('module_type_name', 'N/A'), - "IO Type": addr_info.get("type", "?"), - "IO Address": siemens_addr, - "Number of Bits": length_bits, - "SortKey": ( - "PLC Local Modules", # Network name for sorting - [0], # Device sort key (always first) - ( - int(addr_info.get("module_pos", "9999")) - if str(addr_info.get("module_pos", "9999")).isdigit() - else 9999 + plc_local_modules_io.append( + { + "Network": "PLC Local Modules", + "Network Type": "Local I/O", + "Device Address": "Local", + "Device Name": f"PLC {plc_info.get('name', target_plc_id)}", + "Sub-Device": addr_info.get("module_name", "?"), + "Sub-Device OrderNo": addr_info.get( + "module_order_number", "N/A" ), - addr_info.get("module_name", ""), - addr_info.get("type", ""), - ( - int(addr_info.get("start", "0")) - if str(addr_info.get("start", "0")).isdigit() - else float("inf") + "Sub-Device Type": addr_info.get( + "module_type_name", "N/A" ), - ) - }) - + "IO Type": addr_info.get("type", "?"), + "IO Address": siemens_addr, + "Number of Bits": length_bits, + "SortKey": ( + "PLC Local Modules", # Network name for sorting + [0], # Device sort key (always first) + ( + int(addr_info.get("module_pos", "9999")) + if str( + addr_info.get("module_pos", "9999") + ).isdigit() + else 9999 + ), + addr_info.get("module_name", ""), + addr_info.get("type", ""), + ( + int(addr_info.get("start", "0")) + if str(addr_info.get("start", "0")).isdigit() + else float("inf") + ), + ), + } + ) + # Add PLC local modules to the main IO list all_plc_io_for_table.extend(plc_local_modules_io) @@ -1115,13 +1218,17 @@ def generate_markdown_tree(project_data, md_file_path, target_plc_id, output_roo if module_name not in modules_by_name: modules_by_name[module_name] = [] modules_by_name[module_name].append(io_data) - + for module_name in sorted(modules_by_name.keys()): module_ios = modules_by_name[module_name] first_io = module_ios[0] - markdown_lines.append(f" - **{module_name}** (Type: `{first_io['Sub-Device Type']}`, OrderNo: `{first_io['Sub-Device OrderNo']}`)") - for io_data in sorted(module_ios, key=lambda x: x['SortKey']): - markdown_lines.append(f" - `{io_data['IO Address']}` ({io_data['IO Type']}, {io_data['Number of Bits']} bits)") + markdown_lines.append( + f" - **{module_name}** (Type: `{first_io['Sub-Device Type']}`, OrderNo: `{first_io['Sub-Device OrderNo']}`)" + ) + for io_data in sorted(module_ios, key=lambda x: x["SortKey"]): + markdown_lines.append( + f" - `{io_data['IO Address']}` ({io_data['IO Type']}, {io_data['Number of Bits']} bits)" + ) plc_networks = plc_info.get("connected_networks", {}) markdown_lines.append("\n- **Networks:**") @@ -1148,9 +1255,7 @@ def generate_markdown_tree(project_data, md_file_path, target_plc_id, output_roo markdown_lines.append( f" - ### {net_info.get('name', net_id)} ({net_info.get('type', 'Unknown')})" ) - markdown_lines.append( - f" - PLC Address on this Net: `{plc_addr_on_net}`" - ) + markdown_lines.append(f" - PLC Address on this Net: `{plc_addr_on_net}`") markdown_lines.append(f" - **Devices on Network:**") devices_on_this_net = net_info.get("devices_on_net", {}) @@ -1171,7 +1276,7 @@ def generate_markdown_tree(project_data, md_file_path, target_plc_id, output_roo ) if interface_id_lookup: plc_interface_and_node_ids.add(interface_id_lookup) - plc_interface_and_node_ids.add(target_plc_id) # Use target_plc_id here + plc_interface_and_node_ids.add(target_plc_id) # Use target_plc_id here other_device_items = sorted( [ @@ -1198,7 +1303,7 @@ def generate_markdown_tree(project_data, md_file_path, target_plc_id, output_roo continue interface_id = node_info.get("parent_id") - interface_info_dev = None # Renamed to avoid conflict + interface_info_dev = None # Renamed to avoid conflict actual_device_id = None actual_device_info = None rack_id = None @@ -1261,31 +1366,24 @@ def generate_markdown_tree(project_data, md_file_path, target_plc_id, output_roo markdown_lines.append(f" - {title_str}") # Display Basic Details - markdown_lines.append( - f" - Address (on net): `{node_addr}`" - ) - type_name_disp = display_info_title.get("type_name", "N/A") - order_num_disp = display_info_title.get("order_number", "N/A") - pos_disp = display_info_title.get("position", "N/A") - if type_name_disp and type_name_disp != "N/A": + markdown_lines.append(f" - Address (on net): `{node_addr}`") + type_name_disp = display_info_title.get("type_name", "") + order_num_disp = display_info_title.get("order_number", "") + pos_disp = display_info_title.get("position", "") + if type_name_disp and type_name_disp != "": markdown_lines.append( f" - Type Name: `{type_name_disp}`" ) - if order_num_disp and order_num_disp != "N/A": - markdown_lines.append( - f" - Order No: `{order_num_disp}`" - ) - if pos_disp and pos_disp != "N/A": + if order_num_disp and order_num_disp != "": + markdown_lines.append(f" - Order No: `{order_num_disp}`") + if pos_disp and pos_disp != "": markdown_lines.append( f" - Pos (in parent): `{pos_disp}`" ) ultimate_parent_id = rack_id if not ultimate_parent_id and actual_device_info: ultimate_parent_id = actual_device_info.get("parent_id") - if ( - ultimate_parent_id - and ultimate_parent_id != display_id_title - ): + if ultimate_parent_id and ultimate_parent_id != display_id_title: ultimate_parent_info = project_data.get("devices", {}).get( ultimate_parent_id ) @@ -1296,7 +1394,7 @@ def generate_markdown_tree(project_data, md_file_path, target_plc_id, output_roo ) markdown_lines.append( f" - Parent Structure: `{ultimate_parent_name}`" - ) + ) # --- IO Aggregation Logic (from v24) --- aggregated_io_addresses = [] @@ -1325,18 +1423,20 @@ def generate_markdown_tree(project_data, md_file_path, target_plc_id, output_roo for dev_scan_id, dev_scan_info in project_data.get( "devices", {} ).items(): - if ( - dev_scan_info.get("parent_id") == parent_structure_id - ): + if dev_scan_info.get("parent_id") == parent_structure_id: # This dev_scan_info is the module module_context_for_sibling = { "id": dev_scan_id, "name": dev_scan_info.get("name", dev_scan_id), - "order_number": dev_scan_info.get("order_number", "N/A"), - "type_name": dev_scan_info.get("type_name", "N/A") + "order_number": dev_scan_info.get( + "order_number", "" + ), + "type_name": dev_scan_info.get("type_name", ""), } io_from_sibling = find_io_recursively( - dev_scan_id, project_data, module_context_for_sibling + dev_scan_id, + project_data, + module_context_for_sibling, ) if io_from_sibling: aggregated_io_addresses.extend(io_from_sibling) @@ -1353,8 +1453,8 @@ def generate_markdown_tree(project_data, md_file_path, target_plc_id, output_roo module_context_for_root = { "id": io_search_root_id, "name": io_search_root_info.get("name", io_search_root_id), - "order_number": io_search_root_info.get("order_number", "N/A"), - "type_name": io_search_root_info.get("type_name", "N/A") + "order_number": io_search_root_info.get("order_number", ""), + "type_name": io_search_root_info.get("type_name", ""), } aggregated_io_addresses = find_io_recursively( io_search_root_id, project_data, module_context_for_root @@ -1373,42 +1473,68 @@ def generate_markdown_tree(project_data, md_file_path, target_plc_id, output_roo if aggregated_io_addresses: markdown_lines.append( f" - **IO Addresses (Aggregated from Structure):**" - ) + ) sorted_agg_io = sorted( aggregated_io_addresses, key=lambda x: ( ( int(x.get("module_pos", "9999")) - if str(x.get("module_pos", "9999")).isdigit() # Ensure it's a string before isdigit + if str( + x.get("module_pos", "9999") + ).isdigit() # Ensure it's a string before isdigit else 9999 ), x.get("module_name", ""), x.get("type", ""), ( int(x.get("start", "0")) - if str(x.get("start", "0")).isdigit() # Ensure it's a string + if str( + x.get("start", "0") + ).isdigit() # Ensure it's a string else float("inf") ), ), ) - last_module_id_processed = None # Use the actual module ID for grouping + last_module_id_processed = ( + None # Use the actual module ID for grouping + ) for addr_info in sorted_agg_io: current_module_id_for_grouping = addr_info.get("module_id") - if current_module_id_for_grouping != last_module_id_processed: - module_name_disp = addr_info.get('module_name','?') - module_type_name_disp = addr_info.get('module_type_name', 'N/A') - module_order_num_disp = addr_info.get('module_order_number', 'N/A') + if ( + current_module_id_for_grouping + != last_module_id_processed + ): + module_name_disp = addr_info.get("module_name", "?") + module_type_name_disp = addr_info.get( + "module_type_name", "" + ) + module_order_num_disp = addr_info.get( + "module_order_number", "" + ) module_line_parts = [f"**{module_name_disp}**"] - if module_type_name_disp and module_type_name_disp != 'N/A': - module_line_parts.append(f"Type: `{module_type_name_disp}`") - if module_order_num_disp and module_order_num_disp != 'N/A': - module_line_parts.append(f"OrderNo: `{module_order_num_disp}`") - - # Removed (Pos: ...) from this line as requested - markdown_lines.append(f" - {', '.join(module_line_parts)}") - last_module_id_processed = current_module_id_for_grouping + if ( + module_type_name_disp + and module_type_name_disp != "" + ): + module_line_parts.append( + f"Type: `{module_type_name_disp}`" + ) + if ( + module_order_num_disp + and module_order_num_disp != "" + ): + module_line_parts.append( + f"OrderNo: `{module_order_num_disp}`" + ) + # Removed (Pos: ...) from this line as requested + markdown_lines.append( + f" - {', '.join(module_line_parts)}" + ) + last_module_id_processed = ( + current_module_id_for_grouping + ) # --- Siemens IO Formatting (from v25.1 - keep fixes) --- io_type = addr_info.get("type", "?") @@ -1432,39 +1558,51 @@ def generate_markdown_tree(project_data, md_file_path, target_plc_id, output_roo elif io_type.lower() == "output": prefix = "AW" siemens_addr = f"{prefix} {start_byte}..{end_byte}" - except Exception: - siemens_addr = ( - f"FMT_ERROR({start_str},{length_str})" - ) + except Exception: + siemens_addr = f"FMT_ERROR({start_str},{length_str})" # v31: Collect data for the summary table (Corrected Indentation) - current_device_io_for_table.append({ - "Network": net_info.get('name', net_id), - "Network Type": net_info.get('type', 'Unknown'), - "Device Address": node_addr, - "Device Name": display_name, # Main device name - "Sub-Device": addr_info.get('module_name','?'), # Module name - "Sub-Device OrderNo": addr_info.get('module_order_number', 'N/A'), - "Sub-Device Type": addr_info.get('module_type_name', 'N/A'), - "IO Type": io_type, - "IO Address": siemens_addr, - "Number of Bits": length_bits, - "SortKey": ( # Add a sort key for the table - net_info.get('name', net_id), - sort_key((node_id, node_addr)), # Reuse the device sort key - ( - int(addr_info.get("module_pos", "9999")) - if str(addr_info.get("module_pos", "9999")).isdigit() - else 9999 + current_device_io_for_table.append( + { + "Network": net_info.get("name", net_id), + "Network Type": net_info.get("type", "Unknown"), + "Device Address": node_addr, + "Device Name": display_name, # Main device name + "Sub-Device": addr_info.get( + "module_name", "?" + ), # Module name + "Sub-Device OrderNo": addr_info.get( + "module_order_number", "" ), - addr_info.get("module_name", ""), - io_type, - ( - int(addr_info.get("start", "0")) - if str(addr_info.get("start", "0")).isdigit() - else float("inf") + "Sub-Device Type": addr_info.get( + "module_type_name", "" ), - ) - }) + "IO Type": io_type, + "IO Address": siemens_addr, + "Number of Bits": length_bits, + "SortKey": ( # Add a sort key for the table + net_info.get("name", net_id), + sort_key( + (node_id, node_addr) + ), # Reuse the device sort key + ( + int(addr_info.get("module_pos", "9999")) + if str( + addr_info.get("module_pos", "9999") + ).isdigit() + else 9999 + ), + addr_info.get("module_name", ""), + io_type, + ( + int(addr_info.get("start", "0")) + if str( + addr_info.get("start", "0") + ).isdigit() + else float("inf") + ), + ), + } + ) markdown_lines.append( f" - `{siemens_addr}` (Len={length_bits} bits)" @@ -1511,15 +1649,15 @@ def generate_markdown_tree(project_data, md_file_path, target_plc_id, output_roo with open(md_file_path, "w", encoding="utf-8") as f: f.write("\n".join(markdown_lines)) print(f"Markdown tree summary written to: {md_file_path}") - + # Generate the separate Hardware.md with the IO summary table if all_plc_io_for_table: hardware_file_path = generate_io_summary_file( - all_plc_io_for_table, - md_file_path, - plc_name_for_title, - project_data, - output_root_path + all_plc_io_for_table, + md_file_path, + plc_name_for_title, + project_data, + output_root_path, ) print(f"IO summary table generated in separate file: {hardware_file_path}") @@ -1570,7 +1708,7 @@ def generate_io_upward_tree(project_data, md_file_path): markdown_lines.append( f"## IO Module: {dev_info.get('name', dev_id)} (ID: {dev_id})" ) - markdown_lines.append(f"- Position: {dev_info.get('position', 'N/A')}") + markdown_lines.append(f"- Position: {dev_info.get('position', '')}") markdown_lines.append("- IO Addresses:") for addr in sorted( dev_info["io_addresses"], @@ -1595,9 +1733,7 @@ def generate_io_upward_tree(project_data, md_file_path): count = 0 while current_id and count < ancestor_limit: ancestor_name = current_info.get("name", "?") if current_info else "?" - ancestor_pos = ( - current_info.get("position", "N/A") if current_info else "N/A" - ) + ancestor_pos = current_info.get("position", "") if current_info else "" markdown_lines.append( f"{indent}└─ {ancestor_name} (ID: {current_id}, Pos: {ancestor_pos})" ) @@ -1665,7 +1801,9 @@ def generate_io_upward_tree(project_data, md_file_path): # --- extract_and_save_global_outputs function (Refactored from process_aml_file) --- -def extract_and_save_global_outputs(aml_file_path, json_output_path, md_upward_output_path): +def extract_and_save_global_outputs( + aml_file_path, json_output_path, md_upward_output_path +): """Extracts data from AML, saves global JSON and IO upward tree, returns project_data.""" # (Unchanged) print(f"Processing AML file: {aml_file_path}") @@ -1685,11 +1823,9 @@ def extract_and_save_global_outputs(aml_file_path, json_output_path, md_upward_o except Exception as e: print(f"ERROR writing JSON file {json_output_path}: {e}") traceback.print_exc() - + # Generate and save the IO upward tree (global) - generate_io_upward_tree( - project_data, md_upward_output_path - ) + generate_io_upward_tree(project_data, md_upward_output_path) return project_data except ET.LxmlError as xml_err: print(f"ERROR parsing XML file {aml_file_path} with lxml: {xml_err}") @@ -1701,14 +1837,35 @@ def extract_and_save_global_outputs(aml_file_path, json_output_path, md_upward_o return None -def select_cax_file(initial_dir=None): # Add initial_dir parameter +def find_aml_file_in_dir(directory): + """Finds a single AML file in the given directory and returns its path.""" + try: + aml_files = list(Path(directory).glob("*.aml")) + + if len(aml_files) == 1: + print(f"Automatically found AML file: {aml_files[0]}") + return aml_files[0] + elif len(aml_files) == 0: + print(f"INFO: No .aml file found in '{directory}'.") + return None + else: + print(f"WARNING: Multiple .aml files found in '{directory}':") + for f in aml_files: + print(f" - {f.name}") + return None + except Exception as e: + print(f"Error when searching for AML file in '{directory}': {e}") + return None + + +def select_cax_file(initial_dir=None): # Add initial_dir parameter """Opens a dialog to select a CAx (XML) export file, starting in the specified directory.""" root = tk.Tk() root.withdraw() file_path = filedialog.askopenfilename( title="Select CAx Export File (AML)", - filetypes=[ ("AML Files", "*.aml"), ("All Files", "*.*")], # Added AML - initialdir=initial_dir # Set the initial directory + filetypes=[("AML Files", "*.aml"), ("All Files", "*.*")], # Added AML + initialdir=initial_dir, # Set the initial directory ) root.destroy() if not file_path: @@ -1733,54 +1890,88 @@ def select_output_directory(): def sanitize_filename(name): """Sanitizes a string to be used as a valid filename or directory name.""" - name = str(name) # Ensure it's a string - name = re.sub(r'[<>:"/\\|?*]', '_', name) # Replace forbidden characters - name = re.sub(r'\s+', '_', name) # Replace multiple whitespace with single underscore - name = name.strip('._') # Remove leading/trailing dots or underscores + name = str(name) # Ensure it's a string + name = re.sub(r'[<>:"/\\|?*]', "_", name) # Replace forbidden characters + name = re.sub( + r"\s+", "_", name + ) # Replace multiple whitespace with single underscore + name = name.strip("._") # Remove leading/trailing dots or underscores return name if name else "Unnamed_Device" + # --- Main Execution --- if __name__ == "__main__": try: configs = load_configuration() working_directory = configs.get("working_directory") + level2_configs = configs.get("level2", {}) + aml_exp_directory = level2_configs.get("aml_exp_directory") + resultados_exp_directory = level2_configs.get("resultados_exp_directory") except Exception as e: print(f"Warning: Could not load configuration (frontend not running): {e}") configs = {} working_directory = None + aml_exp_directory = None + resultados_exp_directory = None - script_version = "v32.5 - Include PLC Local Modules in IO Summary" # Updated version + script_version = "v32.8 - Use empty strings instead of N/A" # Updated version print( f"--- AML (CAx Export) to Hierarchical JSON and Obsidian MD Converter ({script_version}) ---" ) - # Validate working directory with .debug fallback - if not working_directory or not os.path.isdir(working_directory): - print("Working directory not set or invalid in configuration.") + # Determine the CAx directory for input AML + cax_directory = None + if working_directory and aml_exp_directory and os.path.isdir(working_directory): + cax_directory = os.path.join(working_directory, aml_exp_directory) + print(f"Using configured CAx directory for input: {cax_directory}") + # Ensure the directory exists + os.makedirs(cax_directory, exist_ok=True) + else: + print("Working directory or aml_exp_directory not configured or invalid.") print("Using .debug directory as fallback for direct script execution.") - + # Fallback to .debug directory under script location script_dir = os.path.dirname(os.path.abspath(__file__)) debug_dir = os.path.join(script_dir, ".debug") - + # Create .debug directory if it doesn't exist os.makedirs(debug_dir, exist_ok=True) - working_directory = debug_dir - print(f"Using debug directory: {working_directory}") + cax_directory = debug_dir + print(f"Using debug directory: {cax_directory}") + + # Determine results directory for outputs + results_dir = None + if ( + working_directory + and resultados_exp_directory + and os.path.isdir(working_directory) + ): + results_dir = os.path.join(working_directory, resultados_exp_directory) + print(f"Using configured results directory for output: {results_dir}") else: - print(f"Using configured working directory: {working_directory}") + print( + "WARNING: resultados_exp_directory not set. Using CAx directory for all outputs." + ) + results_dir = cax_directory # Fallback + os.makedirs(results_dir, exist_ok=True) - # Use working_directory as the output directory - output_dir = working_directory - print(f"Using Working Directory for Output: {output_dir}") + # The cax_directory is for input and intermediate files. + # The results_dir is for final output files. + print(f"Using Directory for Input: {cax_directory}") + print(f"Using Directory for Output Results: {results_dir}") - # 1. Select Input CAx File, starting in the working directory - # Pass working_directory to the selection function - cax_file_path = select_cax_file(initial_dir=working_directory) + # 1. Try to find a single AML file automatically + cax_file_path = find_aml_file_in_dir(cax_directory) + + # If not found or multiple are found, fall back to manual selection + if not cax_file_path: + print("Could not find a unique AML file. Please select one manually.") + cax_file_path = select_cax_file(initial_dir=cax_directory) # Convert paths to Path objects input_path = Path(cax_file_path) - output_path = Path(output_dir) # Output path is the working directory + output_path = Path(cax_directory) # For intermediate files + results_path = Path(results_dir) # For final result files # Check if input file exists if not input_path.is_file(): @@ -1790,13 +1981,15 @@ if __name__ == "__main__": # Ensure output directory exists (redundant if working_directory is valid, but safe) output_path.mkdir(parents=True, exist_ok=True) - # Construct output file paths within the selected output directory (working_directory) + # Construct output file paths output_json_file = output_path / input_path.with_suffix(".hierarchical.json").name - # Hardware tree MD name is now PLC-specific and handled below - output_md_upward_file = output_path / input_path.with_name(f"{input_path.stem}_IO_Upward_Debug.md") + output_md_upward_file = output_path / input_path.with_name( + f"{input_path.stem}_IO_Upward_Debug.md" + ) print(f"Input AML: {input_path.resolve()}") - print(f"Output Directory: {output_path.resolve()}") + print(f"Intermediate Output Directory: {output_path.resolve()}") + print(f"Results Directory: {results_path.resolve()}") print(f"Output JSON: {output_json_file.resolve()}") print(f"Output IO Debug Tree MD: {output_md_upward_file.resolve()}") @@ -1810,29 +2003,43 @@ if __name__ == "__main__": if project_data: # Now, generate the hardware tree per PLC if not project_data.get("plcs"): - print("\nNo PLCs found in the project data. Cannot generate PLC-specific hardware trees.") + print( + "\nNo PLCs found in the project data. Cannot generate PLC-specific hardware trees." + ) else: - print(f"\nFound {len(project_data['plcs'])} PLC(s). Generating individual hardware trees...") + print( + f"\nFound {len(project_data['plcs'])} PLC(s). Generating individual hardware trees..." + ) for plc_id, plc_data_for_plc in project_data.get("plcs", {}).items(): - plc_name_original = plc_data_for_plc.get('name', plc_id) + plc_name_original = plc_data_for_plc.get("name", plc_id) plc_name_sanitized = sanitize_filename(plc_name_original) - plc_doc_dir = output_path / plc_name_sanitized / "Documentation" + plc_doc_dir = results_path / plc_name_sanitized / "Documentation" plc_doc_dir.mkdir(parents=True, exist_ok=True) hardware_tree_md_filename = f"{input_path.stem}_Hardware_Tree.md" output_plc_md_file = plc_doc_dir / hardware_tree_md_filename - print(f" Generating Hardware Tree for PLC '{plc_name_original}' (ID: {plc_id}) at: {output_plc_md_file.resolve()}") - # Pass output_path as the root directory for Hardware.md placement - generate_markdown_tree(project_data, str(output_plc_md_file), plc_id, str(output_path)) - + print( + f" Generating Hardware Tree for PLC '{plc_name_original}' (ID: {plc_id}) at: {output_plc_md_file.resolve()}" + ) + # Pass results_path as the root directory for Hardware.md placement + generate_markdown_tree( + project_data, str(output_plc_md_file), plc_id, str(results_path) + ) + # Generate Excel IO report for this PLC excel_io_filename = f"{input_path.stem}_IO_Report.xlsx" output_excel_file = plc_doc_dir / excel_io_filename - print(f" Generating Excel IO Report for PLC '{plc_name_original}' (ID: {plc_id}) at: {output_excel_file.resolve()}") - generate_io_excel_report(project_data, str(output_excel_file), plc_id, str(output_path)) + print( + f" Generating Excel IO Report for PLC '{plc_name_original}' (ID: {plc_id}) at: {output_excel_file.resolve()}" + ) + generate_io_excel_report( + project_data, str(output_excel_file), plc_id, str(results_path) + ) else: - print("\nFailed to process AML data. Halting before generating PLC-specific trees.") + print( + "\nFailed to process AML data. Halting before generating PLC-specific trees." + ) - print("\nScript finished.") \ No newline at end of file + print("\nScript finished.") diff --git a/backend/script_groups/IO_adaptation/x3_excel_to_md.py b/backend/script_groups/IO_adaptation/x3_excel_to_md.py index fdf70a8..e83d125 100644 --- a/backend/script_groups/IO_adaptation/x3_excel_to_md.py +++ b/backend/script_groups/IO_adaptation/x3_excel_to_md.py @@ -50,64 +50,64 @@ def load_path_config(): # Obtener la configuración global configs = load_configuration() working_directory = configs.get("working_directory") - + if not working_directory: print("Error: 'working_directory' no se encontró en la configuración.") return None - + if not os.path.isdir(working_directory): print( f"Error: El directorio de trabajo '{working_directory}' no existe o no es un directorio." ) return None - + # Path para el archivo JSON de configuración json_config_path = os.path.join(working_directory, "io_paths_config.json") - + # Si el archivo existe, cargarlo if os.path.exists(json_config_path): try: - with open(json_config_path, 'r', encoding='utf-8') as f: + with open(json_config_path, "r", encoding="utf-8") as f: config = json.load(f) print(f"Configuración de paths cargada desde: {json_config_path}") return config except Exception as e: print(f"Error al cargar el archivo de configuración JSON: {e}") return None - + # Si no existe, crear uno con valores predeterminados default_config = { "paths": [ { "path": "Inputs", "type": "Input", - "no_used_path": "IO Not in Hardware\\InputsMaster" + "no_used_path": "IO Not in Hardware\\InputsMaster", }, { "path": "Outputs", "type": "Output", - "no_used_path": "IO Not in Hardware\\OutputsMaster" + "no_used_path": "IO Not in Hardware\\OutputsMaster", }, { "path": "OutputsFesto", "type": "Output", - "no_used_path": "IO Not in Hardware\\OutputsMaster" + "no_used_path": "IO Not in Hardware\\OutputsMaster", }, { "path": "IO Not in Hardware\\InputsMaster", "type": "Input", - "no_used_path": "IO Not in Hardware\\InputsMaster" + "no_used_path": "IO Not in Hardware\\InputsMaster", }, { "path": "IO Not in Hardware\\OutputsMaster", "type": "Output", - "no_used_path": "IO Not in Hardware\\OutputsMaster" - } + "no_used_path": "IO Not in Hardware\\OutputsMaster", + }, ] } - + try: - with open(json_config_path, 'w', encoding='utf-8') as f: + with open(json_config_path, "w", encoding="utf-8") as f: json.dump(default_config, f, indent=2) print(f"Archivo de configuración creado: {json_config_path}") return default_config @@ -118,19 +118,20 @@ def load_path_config(): def convert_excel_to_markdown_tables(): """ - Busca un archivo Excel en el working_directory o solicita al usuario seleccionarlo, - filtra las entradas según los paths configurados en JSON, - y genera un archivo Markdown con tablas. + Busca un archivo Excel en el directorio configurado, lo procesa y genera + un archivo Markdown con tablas filtradas en el directorio de resultados. """ try: configs = load_configuration() working_directory = configs.get("working_directory") - if not working_directory: - print("Error: 'working_directory' no se encontró en la configuración.") - return - if not os.path.isdir(working_directory): + level2_configs = configs.get("level2", {}) + level3_configs = configs.get("level3", {}) + tags_exp_directory = level3_configs.get("tags_exp_directory", ".") + resultados_exp_directory = level2_configs.get("resultados_exp_directory", ".") + + if not working_directory or not os.path.isdir(working_directory): print( - f"Error: El directorio de trabajo '{working_directory}' no existe o no es un directorio." + f"Error: El directorio de trabajo '{working_directory}' no es válido." ) return except Exception as e: @@ -139,39 +140,49 @@ def convert_excel_to_markdown_tables(): working_directory_abs = os.path.abspath(working_directory) print(f"Usando directorio de trabajo: {working_directory_abs}") - - # Cargar la configuración de paths + path_config = load_path_config() if not path_config: print("Error: No se pudo cargar la configuración de paths.") return - - # Verificar si existe el archivo PLCTags.xlsx en el directorio de trabajo - default_excel_path = os.path.join(working_directory_abs, "PLCTags.xlsx") - - if os.path.exists(default_excel_path): - excel_file_path = default_excel_path - print(f"Usando archivo Excel predeterminado: {excel_file_path}") + + tags_exp_dir_abs = os.path.join(working_directory_abs, tags_exp_directory) + os.makedirs(tags_exp_dir_abs, exist_ok=True) + print(f"Buscando archivos Excel en: {tags_exp_dir_abs}") + + excel_files = [ + f for f in os.listdir(tags_exp_dir_abs) if f.lower().endswith(".xlsx") + ] + excel_file_path = "" + + if len(excel_files) == 1: + excel_file_path = os.path.join(tags_exp_dir_abs, excel_files[0]) + print(f"Archivo Excel encontrado automáticamente: {excel_file_path}") else: - # Solicitar al usuario que seleccione el archivo Excel + if len(excel_files) == 0: + print(f"No se encontraron archivos Excel en '{tags_exp_dir_abs}'.") + else: + print( + f"Se encontraron múltiples archivos Excel en '{tags_exp_dir_abs}'. Por favor seleccione uno." + ) + root = tk.Tk() - root.withdraw() # Ocultar ventana principal - - print("Archivo PLCTags.xlsx no encontrado. Seleccione el archivo Excel exportado de TIA Portal:") + root.withdraw() excel_file_path = filedialog.askopenfilename( title="Seleccione el archivo Excel exportado de TIA Portal", filetypes=[("Excel files", "*.xlsx"), ("All files", "*.*")], - initialdir=working_directory_abs + initialdir=tags_exp_dir_abs, ) - if not excel_file_path: print("No se seleccionó ningún archivo Excel. Saliendo...") return - + print(f"Procesando archivo Excel: {excel_file_path}...") - + + output_dir_abs = os.path.join(working_directory_abs, resultados_exp_directory) + os.makedirs(output_dir_abs, exist_ok=True) output_md_filename = "Master IO Tags.md" - output_md_filepath_abs = os.path.join(working_directory_abs, output_md_filename) + output_md_filepath_abs = os.path.join(output_dir_abs, output_md_filename) markdown_content = [] @@ -193,62 +204,70 @@ def convert_excel_to_markdown_tables(): try: # Leer el Excel exportado de TIA Portal excel_data = pd.read_excel(excel_file_path, sheet_name=0) - + # Verificar columnas requeridas excel_col_name = "Name" excel_col_path = "Path" excel_col_data_type = "Data Type" excel_col_comment = "Comment" - + excel_required_cols = [ excel_col_name, excel_col_path, excel_col_data_type, excel_col_comment, ] - - missing_cols = [col for col in excel_required_cols if col not in excel_data.columns] + + missing_cols = [ + col for col in excel_required_cols if col not in excel_data.columns + ] if missing_cols: - print(f"Error: Columnas faltantes en el archivo Excel: {', '.join(missing_cols)}") + print( + f"Error: Columnas faltantes en el archivo Excel: {', '.join(missing_cols)}" + ) return - + # Organizar entradas por path y crear tablas para cada tipo for path_entry in path_config["paths"]: path_name = path_entry["path"] io_type = path_entry["type"] # Input u Output - + # Filtrar datos por el path actual path_data = excel_data[excel_data[excel_col_path] == path_name] - + if path_data.empty: print(f"No se encontraron entradas para el path: {path_name}") continue - + # Agregar encabezado para este path markdown_content.append(f"## {path_name} ({io_type}s)\n") markdown_content.append(markdown_table_header) markdown_content.append(markdown_table_separator) - + # Procesar cada entrada en este path for _, row in path_data.iterrows(): master_tag = str(row.get(excel_col_name, "")) data_type = str(row.get(excel_col_data_type, "")) comment_text = str(row.get(excel_col_comment, "")) description = f'"{comment_text}"' - + # Usar el tipo del path desde la configuración tag_type_for_md = io_type - + master_tag_cell = f"{master_tag:<{col_widths['Master Tag']}.{col_widths['Master Tag']}}" - type_cell = f"{tag_type_for_md:<{col_widths['Type']}.{col_widths['Type']}}" - data_type_cell = f"{data_type:<{col_widths['Data Type']}.{col_widths['Data Type']}}" + type_cell = ( + f"{tag_type_for_md:<{col_widths['Type']}.{col_widths['Type']}}" + ) + data_type_cell = ( + f"{data_type:<{col_widths['Data Type']}.{col_widths['Data Type']}}" + ) description_cell = f"{description:<{col_widths['Description']}.{col_widths['Description']}}" - + md_row = f"| {master_tag_cell} | {type_cell} | {data_type_cell} | {description_cell} |" markdown_content.append(md_row) - + markdown_content.append("\n") # Espacio después de cada tabla - + except FileNotFoundError: print(f"Error: El archivo '{excel_file_path}' no se encontró.") return @@ -263,9 +282,7 @@ def convert_excel_to_markdown_tables(): try: with open(output_md_filepath_abs, "w", encoding="utf-8") as f: f.write("\n".join(markdown_content)) - print( - f"¡Éxito! Archivo Excel convertido a Markdown en: {output_md_filepath_abs}" - ) + print(f"¡Éxito! Archivo Markdown generado en: {output_md_filepath_abs}") except IOError as e: print( f"Error al escribir el archivo Markdown '{output_md_filepath_abs}': {e}" @@ -275,4 +292,4 @@ def convert_excel_to_markdown_tables(): if __name__ == "__main__": - convert_excel_to_markdown_tables() \ No newline at end of file + convert_excel_to_markdown_tables() diff --git a/backend/script_groups/IO_adaptation/x4_prompt_generator.py b/backend/script_groups/IO_adaptation/x4_prompt_generator.py index 39369a9..4437dcf 100644 --- a/backend/script_groups/IO_adaptation/x4_prompt_generator.py +++ b/backend/script_groups/IO_adaptation/x4_prompt_generator.py @@ -71,6 +71,14 @@ def generate_prompt(): f"Error: El directorio de trabajo '{working_directory}' no existe o no es un directorio." ) return False + + group_config = configs.get("level2", {}) + resultados_exp_directory = group_config.get("resultados_exp_directory") + if not resultados_exp_directory: + print( + "Error: 'resultados_exp_directory' no se encontró en la configuración de nivel 2." + ) + return False except Exception as e: print(f"Error al cargar la configuración: {e}") return False @@ -85,8 +93,8 @@ def generate_prompt(): # Intentar obtener la carpeta base de Obsidian desde la configuración try: - # Obtener configuración del nivel 2 - group_config = configs.get("level2", {}) + # Obtener configuración del nivel 3 + group_config = configs.get("level3", {}) obsidian_dir = group_config.get("ObsideanDir") obsidian_projects_base = group_config.get("ObsideanProjectsBase") @@ -146,8 +154,9 @@ def generate_prompt(): print(f"Usando carpeta de equivalencias en Obsidian: {obsidian_mixer_path}") # Definir las rutas a los archivos - master_table_path = os.path.join(working_directory_abs, "Master IO Tags.md") - hardware_table_path = os.path.join(working_directory_abs, "Hardware.md") + data_directory = os.path.join(working_directory_abs, resultados_exp_directory) + master_table_path = os.path.join(data_directory, "Master IO Tags.md") + hardware_table_path = os.path.join(data_directory, "Hardware.md") adaptation_table_path = os.path.join(working_directory_abs, "IO Adapted.md") # Rutas a los archivos de datos semánticos @@ -215,8 +224,8 @@ $Working_Directory = "{working_directory_abs}" $Obsidean_Base_Folder = "{obsidian_mixer_path}" # Archivos de entrada -$Master_table = $Working_Directory + "/Master IO Tags.md" -$Hardware_table = $Working_Directory + "/Hardware.md" +$Master_table = $Working_Directory + "/{resultados_exp_directory}/Master IO Tags.md" +$Hardware_table = $Working_Directory + "/{resultados_exp_directory}/Hardware.md" # Archivo de salida $Adaptation_table = $Working_Directory + "/IO Adapted.md" @@ -370,7 +379,7 @@ def copy_adapted_file_to_obsidian(): return False # Obtener la ruta de destino en Obsidian - group_config = configs.get("level2", {}) + group_config = configs.get("level3", {}) obsidian_dir = group_config.get("ObsideanDir") obsidian_projects_base = group_config.get("ObsideanProjectsBase") diff --git a/data/launcher_history.json b/data/launcher_history.json index 58f4e11..c9ee440 100644 --- a/data/launcher_history.json +++ b/data/launcher_history.json @@ -1,5 +1,18 @@ { "history": [ + { + "id": "8f721c30", + "group_id": "2", + "script_name": "main.py", + "executed_date": "2025-07-14T10:11:20.750196Z", + "arguments": [], + "working_directory": "D:/Proyectos/Scripts/RS485/MaselliSimulatorApp", + "python_env": "tia_scripting", + "executable_type": "pythonw.exe", + "status": "running", + "pid": 22396, + "execution_time": null + }, { "id": "b321622a", "group_id": "4", diff --git a/data/log.txt b/data/log.txt index fb19c14..c15aaa7 100644 --- a/data/log.txt +++ b/data/log.txt @@ -1,290 +1,21 @@ -[16:38:15] Iniciando ejecución de x2_full_io_documentation.py en C:\Trabajo\SIDEL\13 - E5.007560 - Modifica O&U - SAE235\Reporte\Analisis\TwinCat... -[16:38:16] 🚀 Iniciando documentación completa de IOs de TwinCAT -[16:38:16] ================================================================================ -[16:38:16] 📁 Directorio de trabajo: C:\Trabajo\SIDEL\13 - E5.007560 - Modifica O&U - SAE235\Reporte\Analisis\TwinCat -[16:38:16] 📁 Directorio de resultados: C:\Trabajo\SIDEL\13 - E5.007560 - Modifica O&U - SAE235\Reporte\Analisis\TwinCat\result -[16:38:16] 🔍 Escaneando definiciones TwinCAT activas en: C:\Trabajo\SIDEL\13 - E5.007560 - Modifica O&U - SAE235\Reporte\Analisis\TwinCat\scl -[16:38:16] ✅ Encontradas 141 definiciones de IO activas. -[16:38:16] 🔍 Buscando usos de variables definidas en: C:\Trabajo\SIDEL\13 - E5.007560 - Modifica O&U - SAE235\Reporte\Analisis\TwinCat\scl -[16:38:16] 📄 Analizando uso en: ADSVARREAD.scl -[16:38:16] 📄 Analizando uso en: ADSVARTRANSLATE.scl -[16:38:16] 📄 Analizando uso en: ADSVARWRITE.scl -[16:38:16] 📄 Analizando uso en: AMMONIACTRL.scl -[16:38:16] 📄 Analizando uso en: ARRAYTOREAL.scl -[16:38:16] 📄 Analizando uso en: BLENDERPROCEDURE_VARIABLES.scl -[16:38:16] 📄 Analizando uso en: BLENDERRINSE.scl -[16:38:16] 📄 Analizando uso en: BLENDER_PID_CTRL_LOOP.scl -[16:38:17] 📄 Analizando uso en: BLENDER_PROCEDURECALL.scl -[16:38:17] 📄 Analizando uso en: BLENDER_RUNCONTROL.scl -[16:38:17] 📄 Analizando uso en: BLENDER_VARIABLES.scl -[16:38:17] 📄 Analizando uso en: BLENDFILLRECSTRUCT.scl -[16:38:17] 📄 Analizando uso en: BLENDFILLSENDSTRUCT.scl -[16:38:17] 📄 Analizando uso en: BLENDFILLSYSTEM_STARTUP.scl -[16:38:17] 📄 Analizando uso en: BRIXTRACKING.scl -[16:38:17] 📄 Analizando uso en: BYTES_TO_DWORD.scl -[16:38:17] 📄 Analizando uso en: BYTES_TO_WORD.scl -[16:38:17] 📄 Analizando uso en: CALC_INJPRESS.scl -[16:38:17] 📄 Analizando uso en: CARBOWATERLINE.scl -[16:38:17] 📄 Analizando uso en: CENTRALCIP_CTRL.scl -[16:38:17] 📄 Analizando uso en: CETRIFUGAL_HEAD.scl -[16:38:17] 📄 Analizando uso en: CIPRECEIVESTRUCT.scl -[16:38:17] 📄 Analizando uso en: CIPSENDSTRUCT.scl -[16:38:17] 📄 Analizando uso en: CIP_CVQ.scl -[16:38:17] 📄 Analizando uso en: CIP_LINK_TYPE.scl -[16:38:17] 📄 Analizando uso en: CIP_LIST_ELEMENT.scl -[16:38:17] 📄 Analizando uso en: CIP_MAIN.scl -[16:38:18] 📄 Analizando uso en: CIP_PROGRAM_VARIABLES.scl -[16:38:18] 📄 Analizando uso en: CIP_SIMPLE_TYPE.scl -[16:38:18] 📄 Analizando uso en: CIP_STEP_TYPE.scl -[16:38:18] 📄 Analizando uso en: CIP_WAITEVENT_TYPE.scl -[16:38:18] 📄 Analizando uso en: CLEANBOOLARRAY.scl -[16:38:18] 📄 Analizando uso en: CLOCK_SIGNAL.scl -[16:38:18] 📄 Analizando uso en: CLOCK_VARIABLES.scl -[16:38:18] 📄 Analizando uso en: CO2EQPRESS.scl -[16:38:18] 📄 Analizando uso en: CO2INJPRESSURE.scl -[16:38:18] 📄 Analizando uso en: CO2_SOLUBILITY.scl -[16:38:18] 📄 Analizando uso en: CONVERTREAL.scl -[16:38:18] 📄 Analizando uso en: CVQ_0_6_PERC.scl -[16:38:18] 📄 Analizando uso en: CVQ_1P7_8_PERC.scl -[16:38:18] 📄 Analizando uso en: DATA_FROM_CIP.scl -[16:38:18] 📄 Analizando uso en: DATA_TO_CIP.scl -[16:38:18] 📄 Analizando uso en: DEAIRCO2TEMPCOMP.scl -[16:38:18] 📄 Analizando uso en: DEAIREATIONVALVE.scl -[16:38:18] 📄 Analizando uso en: DEAIREATOR_STARTUP.scl -[16:38:18] 📄 Analizando uso en: DELAY.scl -[16:38:18] 📄 Analizando uso en: DELTAP.scl -[16:38:18] 📄 Analizando uso en: DENSIMETER_CALIBRATION.scl -[16:38:18] 📄 Analizando uso en: DERIVE.scl -[16:38:18] 📄 Analizando uso en: DEVICENET_VARIABLES.scl -[16:38:18] 📄 Analizando uso en: DWORD_TO_BYTES.scl -[16:38:18] 📄 Analizando uso en: EXEC_SIMPLE_CIP.scl -[16:38:18] 📄 Analizando uso en: FASTRINSE.scl -[16:38:18] 📄 Analizando uso en: FB41_PIDCONTROLLER.scl -[16:38:18] 📄 Analizando uso en: FC_CONTROL_WORD.scl -[16:38:18] 📄 Analizando uso en: FC_STATUS_WORD.scl -[16:38:18] 📄 Analizando uso en: FEEDFORWARD.scl -[16:38:18] 📄 Analizando uso en: FILLERHEAD.scl -[16:38:18] 📄 Analizando uso en: FILLERRECEIVESTRUCT.scl -[16:38:18] 📄 Analizando uso en: FILLERRINSE.scl -[16:38:18] 📄 Analizando uso en: FILLERRINSETANK_CTRL.scl -[16:38:18] 📄 Analizando uso en: FILLERSENDSTRUCT.scl -[16:38:18] 📄 Analizando uso en: FILLER_CONTROL.scl -[16:38:19] 📄 Analizando uso en: FILLINGTIME.scl -[16:38:19] 📄 Analizando uso en: FIRSTPRODUCTION.scl -[16:38:19] 📄 Analizando uso en: FLOW_TO_PRESS_LOSS.scl -[16:38:19] 📄 Analizando uso en: FREQ_TO_MMH2O.scl -[16:38:19] 📄 Analizando uso en: FRICTIONLOSS.scl -[16:38:19] 📄 Analizando uso en: GETPRODBRIXCO2_FROMANALOGINPUT.scl -[16:38:19] 📄 Analizando uso en: GETPRODO2_FROMANALOGINPUT.scl -[16:38:19] 📄 Analizando uso en: GLOBAL_ALARMS.scl -[16:38:19] 📄 Analizando uso en: GLOBAL_VARIABLES_IN_OUT.scl -[16:38:19] 📄 Analizando uso en: HMI_ALARMS.scl -[16:38:19] 📄 Analizando uso en: HMI_BLENDER_PARAMETERS.scl -[16:38:19] 📄 Analizando uso en: HMI_IO_SHOWING.scl -[16:38:19] 📄 Analizando uso en: HMI_LOCAL_CIP_VARIABLES.scl -[16:38:19] 📄 Analizando uso en: HMI_SERVICE.scl -[16:38:19] 📄 Analizando uso en: HMI_VARIABLES_CMD.scl -[16:38:19] 📄 Analizando uso en: HMI_VARIABLES_STATUS.scl -[16:38:19] 📄 Analizando uso en: INPUT.scl -[16:38:19] 📄 Analizando uso en: INPUT_CIP_SIGNALS.scl -[16:38:20] 📄 Analizando uso en: INPUT_SIGNAL.scl -[16:38:20] 📄 Analizando uso en: INTEGRAL.scl -[16:38:20] 📄 Analizando uso en: LOCALCIP_CTRL.scl -[16:38:20] 📄 Analizando uso en: LOWPASSFILTER.scl -[16:38:20] 📄 Analizando uso en: LOWPASSFILTEROPT.scl -[16:38:20] 📄 Analizando uso en: MASELLI.scl -[16:38:20] 📄 Analizando uso en: MASELLIOPTO_TYPE.scl -[16:38:20] 📄 Analizando uso en: MASELLIUC05_TYPE.scl -[16:38:20] 📄 Analizando uso en: MASELLIUR22_TYPE.scl -[16:38:20] 📄 Analizando uso en: MASELLI_CONTROL.scl -[16:38:20] 📄 Analizando uso en: MAXCARBOCO2_VOL.scl -[16:38:20] 📄 Analizando uso en: MESSAGESCROLL.scl -[16:38:20] 📄 Analizando uso en: MESSAGE_SCROLL.scl -[16:38:20] 📄 Analizando uso en: MFMANALOG_VALUES.scl -[16:38:20] 📄 Analizando uso en: MFM_REAL_STRUCT.scl -[16:38:20] 📄 Analizando uso en: MMH2O_TO_FREQ.scl -[16:38:20] 📄 Analizando uso en: MODVALVEFAULT.scl -[16:38:20] 📄 Analizando uso en: MOVEARRAY.scl -[16:38:20] 📄 Analizando uso en: MPDS1000.scl -[16:38:20] 📄 Analizando uso en: MPDS1000_CONTROL.scl -[16:38:20] 📄 Analizando uso en: MPDS1000_TYPE.scl -[16:38:20] 📄 Analizando uso en: MPDS2000.scl -[16:38:20] 📄 Analizando uso en: MPDS2000_CONTROL.scl -[16:38:20] 📄 Analizando uso en: MPDS2000_TYPE.scl -[16:38:20] 📄 Analizando uso en: MPDS_PA_CONTROL.scl -[16:38:20] 📄 Analizando uso en: MSE_SLOPE.scl -[16:38:20] 📄 Analizando uso en: MYVAR.scl -[16:38:20] 📄 Analizando uso en: OR_ARRAYBOOL.scl -[16:38:20] 📄 Analizando uso en: OUTPUT.scl -[16:38:20] 📄 Analizando uso en: PARAMETERNAMETYPE.scl -[16:38:20] 📄 Analizando uso en: PA_MPDS.scl -[16:38:20] 📄 Analizando uso en: PERIPHERIAL.scl -[16:38:20] 📄 Analizando uso en: PID_VARIABLES.scl -[16:38:20] 📄 Analizando uso en: PLC CONFIGURATION.scl -[16:38:21] 📄 Analizando uso en: PNEUMATIC_VALVE_CTRL.scl -[16:38:21] 📄 Analizando uso en: PPM_O2.scl -[16:38:21] 📄 Analizando uso en: PRODBRIXRECOVERY.scl -[16:38:21] 📄 Analizando uso en: PRODTANK_DRAIN.scl -[16:38:21] 📄 Analizando uso en: PRODTANK_RUNOUT.scl -[16:38:21] 📄 Analizando uso en: PRODUCTAVAILABLE.scl -[16:38:21] 📄 Analizando uso en: PRODUCTION_VARIABLES.scl -[16:38:21] 📄 Analizando uso en: PRODUCTLITERINTANK.scl -[16:38:21] 📄 Analizando uso en: PRODUCTPIPEDRAIN.scl -[16:38:21] 📄 Analizando uso en: PRODUCTPIPERUNOUT.scl -[16:38:21] 📄 Analizando uso en: PRODUCTQUALITY.scl -[16:38:21] 📄 Analizando uso en: PRODUCTTANKBRIX.scl -[16:38:21] 📄 Analizando uso en: PRODUCTTANK_PRESSCTRL.scl -[16:38:21] 📄 Analizando uso en: PROFIBUS_DATA.scl -[16:38:21] 📄 Analizando uso en: PROFIBUS_NETWORK.scl -[16:38:21] 📄 Analizando uso en: PROFIBUS_VARIABLES.scl -[16:38:21] 📄 Analizando uso en: PULSEPRESSURE.scl -[16:38:21] 📄 Analizando uso en: PUMPSCONTROL.scl -[16:38:21] 📄 Analizando uso en: READANALOGIN.scl -[16:38:21] 📄 Analizando uso en: READPERIPHERIAL.scl -[16:38:21] 📄 Analizando uso en: SAFETIES.scl -[16:38:22] 📄 Analizando uso en: SELCHECKBRIXSOURCE.scl -[16:38:22] 📄 Analizando uso en: SIGNALS_INTEFACE.scl -[16:38:22] 📄 Analizando uso en: SIGNAL_GEN.scl -[16:38:22] 📄 Analizando uso en: SINUSOIDAL_SIGNAL.scl -[16:38:22] 📄 Analizando uso en: SLEWLIMIT.scl -[16:38:22] 📄 Analizando uso en: SLIM_BLOCK.scl -[16:38:22] 📄 Analizando uso en: SLIM_VARIABLES.scl -[16:38:22] 📄 Analizando uso en: SOFTNET_VARIABLES.scl -[16:38:22] 📄 Analizando uso en: SPEEDADJUST.scl -[16:38:22] 📄 Analizando uso en: SP_AND_P_VARIABLES.scl -[16:38:22] 📄 Analizando uso en: STANDARD.LIB_5.6.98 09_39_02.scl -[16:38:22] 📄 Analizando uso en: STATISTICALANALISYS.scl -[16:38:22] 📄 Analizando uso en: SYRBRIX_AUTOCORRECTION.scl -[16:38:22] 📄 Analizando uso en: SYRUPDENSITY.scl -[16:38:22] 📄 Analizando uso en: SYRUPROOMCTRL.scl -[16:38:22] 📄 Analizando uso en: SYRUP_LINE_MFM_PREP.scl -[16:38:22] 📄 Analizando uso en: SYRUP_MFM_STARTUP.scl -[16:38:22] 📄 Analizando uso en: SYRUP_RUNOUT.scl -[16:38:22] 📄 Analizando uso en: SYSTEMRUNOUT_VARIABLES.scl -[16:38:22] 📄 Analizando uso en: SYSTEM_DATAS.scl -[16:38:22] 📄 Analizando uso en: SYSTEM_RUN_OUT.scl -[16:38:22] 📄 Analizando uso en: TANKLEVEL.scl -[16:38:22] 📄 Analizando uso en: TANKLEVELTOHEIGHT.scl -[16:38:22] 📄 Analizando uso en: TASK CONFIGURATION.scl -[16:38:22] 📄 Analizando uso en: TCPLCUTILITIES.LIB_11.12.01 09_39_02.scl -[16:38:22] 📄 Analizando uso en: TCSYSTEM.LIB_16.9.02 09_39_02.scl -[16:38:22] 📄 Analizando uso en: TESTFLOWMETERS.scl -[16:38:22] 📄 Analizando uso en: UDP_STRUCT.scl -[16:38:22] 📄 Analizando uso en: UV_LAMP.scl -[16:38:22] 📄 Analizando uso en: VACUUMCTRL.scl -[16:38:22] 📄 Analizando uso en: VALVEFAULT.scl -[16:38:22] 📄 Analizando uso en: VALVEFLOW.scl -[16:38:22] 📄 Analizando uso en: VARIABLE_CONFIGURATION.scl -[16:38:22] 📄 Analizando uso en: VOID.scl -[16:38:22] 📄 Analizando uso en: WATERDENSITY.scl -[16:38:22] 📄 Analizando uso en: WORD_TO_BYTES.scl -[16:38:22] 📄 Analizando uso en: WRITEPERIPHERIAL.scl -[16:38:22] 📄 Analizando uso en: _BLENDER_CTRL_MAIN.scl -[16:38:23] 📄 Analizando uso en: _BLENDER_PID_MAIN.scl -[16:38:23] 📄 Analizando uso en: _BOOLARRAY_TO_DWORD.scl -[16:38:23] 📄 Analizando uso en: _BOOLARRAY_TO_WORD.scl -[16:38:23] 📄 Analizando uso en: _DWORD_SWAP_BYTEARRAY.scl -[16:38:23] 📄 Analizando uso en: _DWORD_TO_BOOLARRAY.scl -[16:38:23] 📄 Analizando uso en: _FILLING_HEAD_PID_CTRL.scl -[16:38:23] 📄 Analizando uso en: _PUMPCONTROL.scl -[16:38:23] 📄 Analizando uso en: _STEPMOVE.scl -[16:38:23] 📄 Analizando uso en: _WORD_TO_BOOLARRAY.scl -[16:38:23] ✅ Encontrados 224 usos para 83 variables distintas. -[16:38:23] 📄 Generando tabla resumen: C:\Trabajo\SIDEL\13 - E5.007560 - Modifica O&U - SAE235\Reporte\Analisis\TwinCat\result\TwinCAT_Full_IO_List.md -[16:38:23] ✅ Tabla resumen generada exitosamente. -[16:38:23] 📄 Generando reporte de snippets: C:\Trabajo\SIDEL\13 - E5.007560 - Modifica O&U - SAE235\Reporte\Analisis\TwinCat\result\TwinCAT_IO_Usage_Snippets.md -[16:38:23] Generando snippets para 83 variables con uso... -[16:38:23] 📝 Procesando 1/83: AI_ProductTankLevel (1 usos) -[16:38:23] 📝 Procesando 2/83: AI_ProductTankPressure (1 usos) -[16:38:23] 📝 Procesando 3/83: AI_DeaireationValve_VEP4 (2 usos) -[16:38:23] 📝 Procesando 4/83: AI_ProdTankPressureValve_VEP1 (1 usos) -[16:38:23] 📝 Procesando 5/83: AI_ProductTemperature (1 usos) -[16:38:23] 📝 Procesando 6/83: AI_SyrupTankLevel (1 usos) -[16:38:23] 📝 Procesando 7/83: AI_DeairWaterTemperature (1 usos) -[16:38:23] 📝 Procesando 8/83: AI_InjectionPressure (2 usos) -[16:38:23] 📝 Procesando 9/83: gProduct_VFC_MainActualValue (1 usos) -[16:38:23] 📝 Procesando 10/83: DI_AuxVoltage_On (1 usos) -[16:38:23] 📝 Procesando 11/83: DI_Reset_Horn_Btn (2 usos) -[16:38:23] 📝 Procesando 12/83: DI_Reset_Btn (79 usos) -[16:38:23] 📝 Procesando 13/83: DI_Blender_Stop_Btn (3 usos) -[16:38:23] 📝 Procesando 14/83: DI_Blender_Start_Btn (1 usos) -[16:38:23] 📝 Procesando 15/83: DI_PowerSuppliesOk (3 usos) -[16:38:23] 📝 Procesando 16/83: DI_Min_Deair_Level (1 usos) -[16:38:23] 📝 Procesando 17/83: DI_ProdTankEmpty (1 usos) -[16:38:23] 📝 Procesando 18/83: DI_BatteryNotReady (1 usos) -[16:38:23] 📝 Procesando 19/83: DI_VM1_Water_Valve_Closed (1 usos) -[16:38:23] 📝 Procesando 20/83: DI_VM2_Syrup_Valve_Closed (1 usos) -[16:38:23] 📝 Procesando 21/83: DI_VM3_CO2_Valve_Closed (1 usos) -[16:38:23] 📝 Procesando 22/83: DI_Water_Pump_Contactor (1 usos) -[16:38:23] 📝 Procesando 23/83: DI_Syrup_Pump_Ovrld (1 usos) -[16:38:23] 📝 Procesando 24/83: DI_Syrup_Pump_Contactor (1 usos) -[16:38:23] 📝 Procesando 25/83: DI_Product_Pump_Contactor (1 usos) -[16:38:23] 📝 Procesando 26/83: DI_SyrRoom_Pump_Ready (1 usos) -[16:38:23] 📝 Procesando 27/83: DI_CIP_CIPMode (1 usos) -[16:38:23] 📝 Procesando 28/83: DI_CIP_RinseMode (1 usos) -[16:38:23] 📝 Procesando 29/83: DI_CIP_DrainRequest (1 usos) -[16:38:23] 📝 Procesando 30/83: DI_CIP_CIPCompleted (1 usos) -[16:38:23] 📝 Procesando 31/83: DI_Air_InletPress_OK (1 usos) -[16:38:23] 📝 Procesando 32/83: DI_Syrup_Line_Drain_Sensor (1 usos) -[16:38:23] 📝 Procesando 33/83: gWaterTotCtrl_Node20 (3 usos) -[16:38:23] 📝 Procesando 34/83: gSyrControl_Node21 (7 usos) -[16:38:23] 📝 Procesando 35/83: gCO2Control_Node22 (7 usos) -[16:38:23] 📝 Procesando 36/83: gProductTotCtrl_Node17 (3 usos) -[16:38:23] 📝 Procesando 37/83: AO_WaterCtrlValve_VM1 (1 usos) -[16:38:23] 📝 Procesando 38/83: AO_SyrupCtrlValve_VM2 (1 usos) -[16:38:23] 📝 Procesando 39/83: AO_CarboCO2CtrlValve_VM3 (1 usos) -[16:38:23] 📝 Procesando 40/83: AO_ProdTankPressureValve_VEP1 (1 usos) -[16:38:23] 📝 Procesando 41/83: AO_DeaireationValve_VEP4 (2 usos) -[16:38:23] 📝 Procesando 42/83: AO_ProdTempCtrlValve (1 usos) -[16:38:23] 📝 Procesando 43/83: AO_SyrupInletValve_VEP3 (1 usos) -[16:38:23] 📝 Procesando 44/83: AO_InjectionPressure (1 usos) -[16:38:23] 📝 Procesando 45/83: gProduct_VFC_MainRefValue (1 usos) -[16:38:23] 📝 Procesando 46/83: DO_SyrupInletValve_Enable (1 usos) -[16:38:23] 📝 Procesando 47/83: DO_HoldBrixMeter (2 usos) -[16:38:23] 📝 Procesando 48/83: DO_SyrupRoomPump_Run (2 usos) -[16:38:23] 📝 Procesando 49/83: DO_SyrupRoomWaterReq (2 usos) -[16:38:23] 📝 Procesando 50/83: DO_CIP_CIPRequest (2 usos) -[16:38:23] 📝 Procesando 51/83: DO_CIP_DrainCompleted (2 usos) -[16:38:23] 📝 Procesando 52/83: DO_Horn (2 usos) -[16:38:23] 📝 Procesando 53/83: DO_Blender_Run_Lamp (2 usos) -[16:38:23] 📝 Procesando 54/83: DO_Alarm_Lamp (2 usos) -[16:38:23] 📝 Procesando 55/83: DO_RotorAlarm_Lamp (2 usos) -[16:38:23] 📝 Procesando 56/83: DO_Water_Pump_Run (2 usos) -[16:38:23] 📝 Procesando 57/83: DO_Syrup_Pump_Run (2 usos) -[16:38:23] 📝 Procesando 58/83: DO_Product_Pump_Run (3 usos) -[16:38:23] 📝 Procesando 59/83: DO_EV11_BlowOff_Valve (2 usos) -[16:38:23] 📝 Procesando 60/83: DO_EV13_Prod_Recirc_Valve (2 usos) -[16:38:23] 📝 Procesando 61/83: DO_EV14_DeairDrain_Valve (2 usos) -[16:38:23] 📝 Procesando 62/83: DO_EV15_ProductTank_Drain_Valve (2 usos) -[16:38:23] 📝 Procesando 63/83: DO_EV16_SyrupTank_Drain_Valve (2 usos) -[16:38:23] 📝 Procesando 64/83: DO_EV17_BufferTankSprayBall_Valve (2 usos) -[16:38:23] 📝 Procesando 65/83: DO_EV18_DeairOverfill_Valve (2 usos) -[16:38:23] 📝 Procesando 66/83: DO_EV21_ProdTankOverfill_Valve (2 usos) -[16:38:23] 📝 Procesando 67/83: DO_EV22_WaterPumpPrime_Valve (2 usos) -[16:38:23] 📝 Procesando 68/83: DO_EV23_SerpentineDrain_valve (2 usos) -[16:38:23] 📝 Procesando 69/83: DO_EV24_SyrupRecirc_Valve (2 usos) -[16:38:23] 📝 Procesando 70/83: DO_EV26_CO2InjShutOff_Valve (2 usos) -[16:38:23] 📝 Procesando 71/83: DO_EV27_DeairSprayBall_Valve (2 usos) -[16:38:23] 📝 Procesando 72/83: DO_EV28_DeairStartCO2Inj_Valve (2 usos) -[16:38:23] 📝 Procesando 73/83: DO_EV44_SyrupLineDrain (2 usos) -[16:38:23] 📝 Procesando 74/83: DO_EV45_ProductChillerDrain (2 usos) -[16:38:23] 📝 Procesando 75/83: DO_EV61_SyrupTankSprayBall (2 usos) -[16:38:23] 📝 Procesando 76/83: DO_EV62_ProductOutlet (3 usos) -[16:38:23] 📝 Procesando 77/83: DO_EV69_Blender_ProductPipeDrain (2 usos) -[16:38:23] 📝 Procesando 78/83: DO_EV81_Prod_Recirc_Chiller_Valve (2 usos) -[16:38:23] 📝 Procesando 79/83: DO_EV01_Deair_Lvl_Ctrl_Valve (2 usos) -[16:38:23] 📝 Procesando 80/83: DO_EV02_Deair_FillUp_Valve (2 usos) -[16:38:23] 📝 Procesando 81/83: gPAmPDSFreeze (2 usos) -[16:38:23] 📝 Procesando 82/83: gPAmPDSCarboStop (2 usos) -[16:38:23] 📝 Procesando 83/83: gPAmPDSInlinePumpStop (2 usos) -[16:38:23] Generando tabla para 58 variables no usadas... -[16:38:23] ✅ Reporte de snippets generado exitosamente. -[16:38:23] 📄 Generando reporte JSON: C:\Trabajo\SIDEL\13 - E5.007560 - Modifica O&U - SAE235\Reporte\Analisis\TwinCat\result\TwinCAT_IO_Usage_Snippets.json -[16:38:23] ✅ Reporte JSON generado exitosamente. -[16:38:23] 🎉 Análisis completado exitosamente! -[16:38:23] 📁 Archivos generados en: C:\Trabajo\SIDEL\13 - E5.007560 - Modifica O&U - SAE235\Reporte\Analisis\TwinCat\result -[16:38:23] 📄 TwinCAT_Full_IO_List.md -[16:38:23] 📄 TwinCAT_IO_Usage_Snippets.md -[16:38:23] 📄 TwinCAT_IO_Usage_Snippets.json -[16:38:23] Ejecución de x2_full_io_documentation.py finalizada (success). Duración: 0:00:07.976669. -[16:38:23] Log completo guardado en: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\TwinCat\log_x2_full_io_documentation.txt +[18:10:38] Iniciando ejecución de x4_prompt_generator.py en C:\Trabajo\SIDEL\13 - E5.007560 - Modifica O&U - SAE235\Reporte\Analisis\Siemens... +[18:10:38] Generador de prompt para adaptación de IO +[18:10:38] ========================================= +[18:10:38] Usando directorio de trabajo: C:\Trabajo\SIDEL\13 - E5.007560 - Modifica O&U - SAE235\Reporte\Analisis\Siemens +[18:10:38] Usando ruta de Obsidian desde configuración: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\00 - MASTER\MIXER\IO +[18:10:38] Usando carpeta de equivalencias en Obsidian: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\00 - MASTER\MIXER\IO +[18:10:38] ¡Prompt generado y copiado al portapapeles con éxito! +[18:10:38] Prompt guardado en: C:\Trabajo\SIDEL\13 - E5.007560 - Modifica O&U - SAE235\Reporte\Analisis\Siemens\IO_Adaptation_Prompt.txt +[18:11:10] Ejecución de x4_prompt_generator.py finalizada (success). Duración: 0:00:31.385093. +[18:11:10] Log completo guardado en: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\IO_adaptation\.log\log_x4_prompt_generator.txt +[10:11:20] Ejecutando script GUI: main.py +[10:11:20] Entorno Python: tia_scripting +[10:11:20] Ejecutable: pythonw.exe (sin logging) +[10:11:20] Comando: C:\Users\migue\miniconda3\envs\tia_scripting\pythonw.exe D:/Proyectos/Scripts/RS485/MaselliSimulatorApp\main.py +[10:11:20] Directorio: D:/Proyectos/Scripts/RS485/MaselliSimulatorApp +[10:11:20] Encoding: UTF-8 (PYTHONUTF8=1, PYTHONIOENCODING=utf-8) +[10:11:20] ================================================== +[10:11:20] Script GUI ejecutado sin logging (pythonw.exe) +[10:11:20] PID: 22396 +[10:11:20] ================================================== +[10:11:20] ID de ejecución: 8f721c30 diff --git a/templates/index.html b/templates/index.html index 4abb45f..e20814b 100644 --- a/templates/index.html +++ b/templates/index.html @@ -227,7 +227,7 @@
-

Configuración del Proyecto

+

Datos de Ingreso