Actualización de directorios de trabajo y mejora en la gestión de logs
- Se modificaron los directorios de trabajo en `script_config.json` y `work_dir.json` para apuntar a la nueva ubicación de los archivos relacionados con el proyecto SIDEL. - Se actualizaron los logs de ejecución en `log_x1.txt` y `log_x4.txt` para reflejar las nuevas fechas, duraciones y resultados de los procesos de exportación. - Se implementó una nueva función de limpieza en `x0_main.py` para eliminar artefactos generados durante la ejecución de los scripts, mejorando la gestión de archivos temporales. - Se realizaron ajustes en la interfaz de usuario para mejorar la experiencia al seleccionar y confirmar directorios de trabajo.
This commit is contained in:
parent
5da7dcad06
commit
f57d0f21dc
|
@ -0,0 +1,293 @@
|
||||||
|
# Guía de Configuración para Scripts Backend
|
||||||
|
|
||||||
|
## Introducción
|
||||||
|
|
||||||
|
Esta guía explica cómo configurar y usar correctamente la función `load_configuration()` en scripts ubicados bajo el directorio `/backend`. La función carga configuraciones desde un archivo `script_config.json` ubicado en el mismo directorio que el script que la llama.
|
||||||
|
|
||||||
|
## Configuración del Path e Importación
|
||||||
|
|
||||||
|
### 1. Configuración estándar del Path
|
||||||
|
|
||||||
|
Para scripts ubicados en subdirectorios bajo `/backend`, usa este patrón estándar:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Configurar el path al directorio raíz del proyecto
|
||||||
|
script_root = os.path.dirname(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
)
|
||||||
|
sys.path.append(script_root)
|
||||||
|
|
||||||
|
# Importar la función de configuración
|
||||||
|
from backend.script_utils import load_configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
**Nota:** El número de `os.path.dirname()` anidados depende de la profundidad del script:
|
||||||
|
- Scripts en `/backend/script_groups/grupo/`: 4 niveles
|
||||||
|
- Scripts en `/backend/`: 2 niveles
|
||||||
|
|
||||||
|
### 2. Importación Correcta
|
||||||
|
|
||||||
|
**✅ Correcto:**
|
||||||
|
```python
|
||||||
|
from backend.script_utils import load_configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
**❌ Incorrecto:**
|
||||||
|
```python
|
||||||
|
from script_utils import load_configuration # No funciona desde subdirectorios
|
||||||
|
```
|
||||||
|
|
||||||
|
## Uso de la Función load_configuration()
|
||||||
|
|
||||||
|
### Implementación Básica
|
||||||
|
|
||||||
|
```python
|
||||||
|
def main():
|
||||||
|
# Cargar configuraciones
|
||||||
|
configs = load_configuration()
|
||||||
|
|
||||||
|
# Obtener el directorio de trabajo
|
||||||
|
working_directory = configs.get("working_directory", "")
|
||||||
|
|
||||||
|
# Acceder a configuraciones por nivel
|
||||||
|
level1_config = configs.get("level1", {})
|
||||||
|
level2_config = configs.get("level2", {})
|
||||||
|
level3_config = configs.get("level3", {})
|
||||||
|
|
||||||
|
# Ejemplo de uso de parámetros específicos con valores por defecto
|
||||||
|
scl_output_dir = level2_config.get("scl_output_dir", "scl_output")
|
||||||
|
xref_output_dir = level2_config.get("xref_output_dir", "xref_output")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Estructura del Archivo script_config.json
|
||||||
|
|
||||||
|
El archivo `script_config.json` debe estar ubicado en el mismo directorio que el script que llama a `load_configuration()`. Estructura recomendada:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"working_directory": "/ruta/al/directorio/de/trabajo",
|
||||||
|
"level1": {
|
||||||
|
"parametro_global_1": "valor1",
|
||||||
|
"parametro_global_2": "valor2"
|
||||||
|
},
|
||||||
|
"level2": {
|
||||||
|
"scl_output_dir": "scl_output",
|
||||||
|
"xref_output_dir": "xref_output",
|
||||||
|
"xref_source_subdir": "source",
|
||||||
|
"aggregated_filename": "full_project_representation.md"
|
||||||
|
},
|
||||||
|
"level3": {
|
||||||
|
"parametro_especifico_1": true,
|
||||||
|
"parametro_especifico_2": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ejemplo Completo de Implementación
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""
|
||||||
|
Script de ejemplo que demuestra el uso completo de load_configuration()
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Configuración del path
|
||||||
|
script_root = os.path.dirname(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
)
|
||||||
|
sys.path.append(script_root)
|
||||||
|
from backend.script_utils import load_configuration
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=== Cargando Configuración ===")
|
||||||
|
|
||||||
|
# Cargar configuraciones
|
||||||
|
configs = load_configuration()
|
||||||
|
|
||||||
|
# Verificar que se cargó correctamente
|
||||||
|
if not configs:
|
||||||
|
print("Error: No se pudo cargar la configuración")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Obtener configuraciones
|
||||||
|
working_directory = configs.get("working_directory", "")
|
||||||
|
level1_config = configs.get("level1", {})
|
||||||
|
level2_config = configs.get("level2", {})
|
||||||
|
level3_config = configs.get("level3", {})
|
||||||
|
|
||||||
|
# Mostrar configuraciones cargadas
|
||||||
|
print(f"Directorio de trabajo: {working_directory}")
|
||||||
|
print("Configuración Nivel 1:", json.dumps(level1_config, indent=2))
|
||||||
|
print("Configuración Nivel 2:", json.dumps(level2_config, indent=2))
|
||||||
|
print("Configuración Nivel 3:", json.dumps(level3_config, indent=2))
|
||||||
|
|
||||||
|
# Ejemplo de uso de parámetros con valores por defecto
|
||||||
|
scl_output_dir = level2_config.get("scl_output_dir", "scl_output")
|
||||||
|
xref_output_dir = level2_config.get("xref_output_dir", "xref_output")
|
||||||
|
|
||||||
|
print(f"Directorio de salida SCL: {scl_output_dir}")
|
||||||
|
print(f"Directorio de salida XREF: {xref_output_dir}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manejo de Errores
|
||||||
|
|
||||||
|
La función `load_configuration()` maneja automáticamente los siguientes casos:
|
||||||
|
|
||||||
|
1. **Archivo no encontrado**: Retorna un diccionario vacío `{}`
|
||||||
|
2. **JSON inválido**: Retorna un diccionario vacío y muestra un mensaje de error
|
||||||
|
3. **Errores de lectura**: Retorna un diccionario vacío y muestra un mensaje de error
|
||||||
|
|
||||||
|
### Verificación de Configuración Válida
|
||||||
|
|
||||||
|
```python
|
||||||
|
configs = load_configuration()
|
||||||
|
|
||||||
|
# Verificar que se cargó correctamente
|
||||||
|
if not configs:
|
||||||
|
print("Advertencia: No se pudo cargar la configuración, usando valores por defecto")
|
||||||
|
working_directory = "."
|
||||||
|
else:
|
||||||
|
working_directory = configs.get("working_directory", ".")
|
||||||
|
|
||||||
|
# Verificar directorio de trabajo
|
||||||
|
if not os.path.exists(working_directory):
|
||||||
|
print(f"Error: El directorio de trabajo no existe: {working_directory}")
|
||||||
|
return
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mejores Prácticas
|
||||||
|
|
||||||
|
1. **Siempre proporciona valores por defecto** al usar `.get()`:
|
||||||
|
```python
|
||||||
|
valor = config.get("clave", "valor_por_defecto")
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Verifica la existencia de directorios críticos**:
|
||||||
|
```python
|
||||||
|
if not os.path.exists(working_directory):
|
||||||
|
print(f"Error: Directorio no encontrado: {working_directory}")
|
||||||
|
return
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Documenta los parámetros esperados** en tu script:
|
||||||
|
```python
|
||||||
|
# Parámetros esperados en level2:
|
||||||
|
# - scl_output_dir: Directorio de salida para archivos SCL
|
||||||
|
# - xref_output_dir: Directorio de salida para referencias cruzadas
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Usa nombres de parámetros consistentes** en todos los scripts del mismo grupo.
|
||||||
|
|
||||||
|
## Definición Técnica de load_configuration()
|
||||||
|
|
||||||
|
```python
|
||||||
|
def load_configuration() -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Load configuration from script_config.json in the current script directory.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict containing configurations with levels 1, 2, 3 and working_directory
|
||||||
|
|
||||||
|
Example usage in scripts:
|
||||||
|
from backend.script_utils import load_configuration
|
||||||
|
|
||||||
|
configs = load_configuration()
|
||||||
|
level1_config = configs.get("level1", {})
|
||||||
|
level2_config = configs.get("level2", {})
|
||||||
|
level3_config = configs.get("level3", {})
|
||||||
|
working_dir = configs.get("working_directory", "")
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
La función utiliza `inspect.stack()` para determinar automáticamente el directorio del script que la llama, asegurando que siempre busque el archivo `script_config.json` en la ubicación correcta.
|
||||||
|
|
||||||
|
## Documentación de Scripts para el Launcher
|
||||||
|
|
||||||
|
El sistema de launcher utiliza archivos JSON para mostrar información sobre los grupos de scripts y scripts individuales en la interfaz web.
|
||||||
|
|
||||||
|
### Archivo description.json (Descripción del Grupo)
|
||||||
|
|
||||||
|
Ubicación: En el directorio raíz del grupo de scripts.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Nombre del Grupo",
|
||||||
|
"description": "Descripción del propósito y funcionalidad del grupo",
|
||||||
|
"version": "1.0",
|
||||||
|
"author": "Nombre del Autor"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Archivo scripts_description.json (Descripción de Scripts)
|
||||||
|
|
||||||
|
Ubicación: En el directorio raíz del grupo de scripts.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nombre_script.py": {
|
||||||
|
"display_name": "Nombre para mostrar en la UI",
|
||||||
|
"short_description": "Descripción breve del script",
|
||||||
|
"long_description": "Descripción detallada con explicación completa de funcionalidad, pasos que ejecuta, y contexto de uso",
|
||||||
|
"hidden": false
|
||||||
|
},
|
||||||
|
"script_interno.py": {
|
||||||
|
"display_name": "Script Interno",
|
||||||
|
"short_description": "Script de uso interno",
|
||||||
|
"long_description": "",
|
||||||
|
"hidden": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Propiedades Importantes
|
||||||
|
|
||||||
|
- **hidden**: `true` oculta el script del launcher (útil para scripts auxiliares)
|
||||||
|
- **display_name**: Nombre amigable que aparece en la interfaz
|
||||||
|
- **short_description**: Se muestra en la lista de scripts
|
||||||
|
- **long_description**: Se muestra al expandir detalles del script
|
||||||
|
|
||||||
|
### Ejemplo Práctico
|
||||||
|
|
||||||
|
Para un grupo "XML Parser to SCL":
|
||||||
|
|
||||||
|
**description.json:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Siemens-Tia : 03 : Procesador de XML LAD-SCL-AWL",
|
||||||
|
"description": "Scripts que procesan archivos XML exportados de TIA, convirtiendo LAD a SCL",
|
||||||
|
"version": "1.0",
|
||||||
|
"author": "Miguel"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**scripts_description.json:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"x0_main.py": {
|
||||||
|
"display_name": "1: Procesar Exportación XML completa",
|
||||||
|
"short_description": "Conversor principal de LAD/FUP XML a SCL",
|
||||||
|
"long_description": "Script orquestador que procesa todos los archivos XML...",
|
||||||
|
"hidden": false
|
||||||
|
},
|
||||||
|
"x1_to_json.py": {
|
||||||
|
"display_name": "x1_to_json",
|
||||||
|
"short_description": "Converter XML interno",
|
||||||
|
"long_description": "",
|
||||||
|
"hidden": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -5,5 +5,5 @@
|
||||||
},
|
},
|
||||||
"level2": {},
|
"level2": {},
|
||||||
"level3": {},
|
"level3": {},
|
||||||
"working_directory": "D:\\Trabajo\\VM\\22 - 93841 - Sidel - Tilting\\Reporte\\TiaExports"
|
"working_directory": "C:\\Trabajo\\SIDEL\\13 - E5.007560 - Modifica O&U - SAE235\\Reporte\\ExportTia"
|
||||||
}
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"path": "D:\\Trabajo\\VM\\22 - 93841 - Sidel - Tilting\\Reporte\\TiaExports",
|
"path": "C:\\Trabajo\\SIDEL\\13 - E5.007560 - Modifica O&U - SAE235\\Reporte\\ExportTia",
|
||||||
"history": [
|
"history": [
|
||||||
|
"C:\\Trabajo\\SIDEL\\13 - E5.007560 - Modifica O&U - SAE235\\Reporte\\ExportTia",
|
||||||
"D:\\Trabajo\\VM\\22 - 93841 - Sidel - Tilting\\Reporte\\TiaExports",
|
"D:\\Trabajo\\VM\\22 - 93841 - Sidel - Tilting\\Reporte\\TiaExports",
|
||||||
"D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source",
|
"D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source",
|
||||||
"C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\SourceDoc\\SourcdSD",
|
"C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\SourceDoc\\SourcdSD",
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
Perfecto. Buscaré documentación, ejemplos y recursos útiles sobre cómo interpretar el archivo .EXP exportado por TwinCAT 2.8, especialmente enfocados en los tokens de lenguaje ladder (LAD) como `_NETWORK`, `_LD_ASSIGN`, `_LD_AND`, `_LD_CONTACT`, etc. Priorizaré repositorios de GitHub y recursos prácticos que ayuden a traducir estos tokens a un pseudocódigo similar a Structured Text (ST), útil para documentación. Te aviso en cuanto tenga los resultados.
|
||||||
|
|
||||||
|
|
||||||
|
# Interpretación de archivos .EXP de TwinCAT 2.8 para lógica Ladder (LD)
|
||||||
|
|
||||||
|
## Formato de exportación ASCII en TwinCAT 2.8
|
||||||
|
|
||||||
|
TwinCAT 2 (basado en CoDeSys 2.x) permite exportar el código PLC a archivos de texto con extensión `.exp`. Dado que los lenguajes gráficos como Ladder Diagram (LD) o Function Block Diagram (FBD) no tienen una representación textual estándar en IEC 61131-3, TwinCAT utiliza un **formato ASCII propio** para exportar estos POUs. En un archivo `.EXP` exportado, cada red (rung) de un diagrama Ladder se describe mediante una secuencia de *tokens* o palabras clave especiales en texto plano. Estos tokens representan contactos, bobinas, operaciones lógicas y la estructura de las redes. Por ejemplo, el manual oficial indica que las POUs en LD/FBD se pueden guardar como ASCII porque “no existe un formato de texto para esto en IEC 61131-3”, por lo que TwinCAT escribe los objetos seleccionados a un archivo ASCII.
|
||||||
|
|
||||||
|
Al exportar en TwinCAT 2.8, puede elegirse exportar cada POU en un archivo separado `<nombre>.exp` (por ejemplo, `Program1.exp`) o combinar todo en un solo archivo. El contenido del `.exp` incluye secciones para variables, listas de símbolos y, lo más importante, el **código Ladder como secuencia de tokens** que representan la lógica. A continuación, detallamos los tokens Ladder más comunes y cómo mapearlos a pseudocódigo legible (similar a Structured Text).
|
||||||
|
|
||||||
|
## Tokens del lenguaje Ladder en archivos .EXP
|
||||||
|
|
||||||
|
A continuación se listan los principales tokens encontrados en un `.exp` exportado de TwinCAT 2.8 para Ladder, junto con su significado e interpretación:
|
||||||
|
|
||||||
|
* **`_NETWORK`** – Indica el inicio de una *red* Ladder (un rung). Cada red Ladder comienza con este token. Puede ir seguida de un identificador de red o comentario de rung. Por ejemplo, un `.exp` típico mostrará cada rung separado iniciando con `_NETWORK`. Si existen comentarios de red, suelen aparecer a continuación.
|
||||||
|
* **`_COMMENT` / `_END_COMMENT`** – Delimitan un bloque de comentario. TwinCAT 2 permitía agregar comentarios por red (rung comment) que en el archivo `.exp` aparecen entre `_COMMENT` y `_END_COMMENT`. Este bloque (si existe) contiene el texto del comentario del rung.
|
||||||
|
* **`_LD_CONTACT`** – Representa un **contacto** en Ladder. Va seguido de la referencia de la variable booleana asociada (p. ej. una entrada, bit interno, etc.). Indica un contacto normalmente abierto por defecto, a menos que se especifique lo contrario con un flag de inversión. Inmediatamente después del `_LD_CONTACT <Variable>` suele aparecer un token `_EXPRESSION` que define si el contacto está invertido o no.
|
||||||
|
* **`_EXPRESSION _POSITIV`** – Este par de tokens suele seguir a un contacto o a una asignación para indicar **polaridad positiva** (no invertido). En el caso de un contacto, `_POSITIV` significa que es un contacto normalmente abierto (pasa la energía cuando la variable es TRUE). Si el contacto fuese normalmente cerrado, aparecería un indicador distinto (por ejemplo, `_NEG` u otro flag en lugar de `_POSITIV` – en la documentación de terceros se describe este campo como *“si está negado”*, siendo `_POSITIV` el valor cuando **no** está negado). En resumen, `_EXPRESSION _POSITIV` después de `_LD_CONTACT Var` confirma que el contacto `Var` se evalúa directamente (TRUE cuando `Var`=TRUE). Si fuera un contacto negado, veríamos un flag indicando inversión (p.ej., `_EXPRESSION _NEG`), lo que implicaría que en pseudocódigo se interpreta como `NOT Var`.
|
||||||
|
* **`_LD_AND`** – Operador lógico **AND** en Ladder. Este token señala que se realiza una conjunción lógica de las condiciones previas en la red. Por ejemplo, si dos contactos en serie alimentan una bobina, en el `.exp` aparecerá un `_LD_AND` para combinar ambos contactos. Generalmente viene acompañado de `_LD_OPERATOR : N`, donde *N* es el número de operandos que está combinando. Un `_LD_AND` con `_LD_OPERATOR : 2` indica que dos condiciones previas se están combinando con AND (es decir, ambas deben ser TRUE). En pseudocódigo, esto equivale a la operación lógica `Cond1 AND Cond2`. De modo similar existe `_LD_OR` (no mostrado arriba pero presente en exportaciones con ramas paralelas) para la operación OR lógico entre ramas.
|
||||||
|
* **`_LD_OR`** – (Aunque no aparece en nuestros ejemplos concretos, es análogo a `_LD_AND`.) Representaría una operación OR entre condiciones/paralelos en la red Ladder. Por ejemplo, contactos en paralelo se exportarían con `_LD_OR` y un `_LD_OPERATOR : N` indicando cuántos caminos paralelos se están OR-combinando.
|
||||||
|
* **`_LD_ASSIGN`** – Marca el **fin de la evaluación lógica de la red** y el inicio de las asignaciones a salidas. Es decir, una vez que se han procesado todos los contactos y operaciones lógicas de la red, `_LD_ASSIGN` indica que ese resultado booleano (TRUE/FALSE) se va a asignar a una o varias bobinas de salida. En la exportación, después de `_LD_ASSIGN` típicamente vendrá otra línea `_EXPRESSION _POSITIV` (o `_NEG`) para indicar si el resultado de la red se utiliza tal cual o invertido para las salidas. Por lo general, será `_POSITIV` salvo que se invierta toda la lógica del rung (situación poco común).
|
||||||
|
* **`_OUTPUTS : N`** – Indica el **número de salidas (bobinas) en esta red**. Si un rung Ladder tiene varias bobinas en paralelo que dependen de la misma lógica de contactos (por ejemplo, bobinas paralelas), aquí se listarán cuántas son. En la mayoría de redes Ladder típicas N=1 (una bobina al final del rung). Un ejemplo del formato exportado: `_OUTPUTS : 1 --1 个输出` significa “1 salida”. Si hubiera, por ejemplo, dos bobinas en paralelo, veríamos `_OUTPUTS : 2`.
|
||||||
|
* **`_OUTPUT`** – Define una **bobina de salida** (coil) a activar. Tras `_OUTPUT` se indican flags que describen el tipo de bobina y su polaridad, seguidos del nombre de la variable de salida asociada. Por ejemplo: `_OUTPUT _POSITIV _NO_SET D0001`. Aquí `_POSITIV` indica que la bobina no está invertida (es una bobina “normalmente desactivada”, energizada directamente con el resultado TRUE de la lógica) y `_NO_SET` indica que es una **bobina regular** (no del tipo Set/Reset). Finalmente `D0001` sería el nombre o dirección de la variable de esa bobina. En un contexto real, en lugar de `D0001` aparecería el nombre de la salida (por ejemplo `MotorOn`) o la dirección (%QX etc., dependiendo de cómo se exporten las variables).
|
||||||
|
|
||||||
|
* **Bobinas Set/Reset:** Si la bobina fuera del tipo *latch* (enganche) de Set/Reset, en lugar de `_NO_SET` aparecería otro token. Por ejemplo, es esperable `_SET` para una bobina de *Set* dominante y `_RESET` para una de *Reset*. En la documentación no oficial se observa que `_NO_SET` se usa para bobinas normales, por lo que presumiblemente existen `_SET`/`_NO_RESET` como flags alternativos. Asimismo, la polaridad `_POSITIV` podría cambiar a `_NEG` si se tratara de una bobina negada (una bobina especial energizada cuando la condición es FALSE). En general: `_OUTPUT _POSITIV _NO_SET Var` corresponde a `Var := Resultado_logico` cuando la lógica es TRUE (bobina estándar), mientras que una variante `_OUTPUT _POSITIV _SET Var` significaría que `Var` se *establece (latchea)* a TRUE con la condición, y `_OUTPUT _POSITIV _RESET Var` que `Var` se resetea con la condición.
|
||||||
|
* **`END_PROGRAM`** – Marca el fin del bloque de programa exportado. El archivo `.exp` típico comienza con la declaración del POU (p. ej. `PROGRAM NombreProg LD`) y finaliza con `END_PROGRAM` una vez listadas todas las redes Ladder y salidas. Todo lo descrito entre estos delimitadores corresponde al contenido del POU Ladder en formato textual.
|
||||||
|
|
||||||
|
**Ejemplo ilustrativo:** En un foro técnico se mostró un fragmento de `.exp` resultante de exportar Ladder, que ayuda a visualizar varios de estos tokens y su secuencia. Por ejemplo:
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
_LD_CONTACT A0001 (... variable de entrada ...)
|
||||||
|
_LD_CONTACT A0002 (... otra entrada ...)
|
||||||
|
_LD_AND
|
||||||
|
_LD_OPERATOR : 2 ; AND de 2 operandos (A0001 y A0002)
|
||||||
|
_LD_ASSIGN
|
||||||
|
_OUTPUTS : 1 ; Una salida en esta red
|
||||||
|
_OUTPUT _POSITIV _NO_SET D0001
|
||||||
|
END_PROGRAM
|
||||||
|
```
|
||||||
|
|
||||||
|
En este caso hipotético, `A0001` y `A0002` podrían ser dos contactos en serie y `D0001` una bobina de salida. Los tokens indican: carga dos contactos (`_LD_CONTACT`), combínalos con un AND de 2 entradas (`_LD_AND` + `_LD_OPERATOR:2`), asigna el resultado (`_LD_ASSIGN`) a 1 salida (`_OUTPUTS:1`), que es una bobina normal no invertida (`_OUTPUT _POSITIV _NO_SET`) asignada a la variable D0001.
|
||||||
|
|
||||||
|
Del mismo modo, otro ejemplo simple tomado de la documentación no oficial muestra la estructura para una red con **un contacto y una bobina** únicamente: primero el contacto y su variable, luego la asignación y la bobina de salida. Allí se observa `_LD_CONTACT p1` seguido de `_EXPRESSION _POSITIV` (contacto normalmente abierto con variable **p1**), luego `_LD_ASSIGN` con `_EXPRESSION _POSITIV` y finalmente `_OUTPUTS:1` con `_OUTPUT _POSITIV _NO_SET p2` para energizar la variable **p2**. Esta red equivale a una lógica donde *p2 = p1*, es decir, la bobina p2 se activa cuando la entrada p1 está activa.
|
||||||
|
|
||||||
|
## Mapeo de la lógica Ladder a pseudocódigo (Structured Text)
|
||||||
|
|
||||||
|
El objetivo de interpretar estos tokens es poder traducir la lógica Ladder en texto entendible, similar a Structured Text (ST) o pseudocódigo, para facilitar la documentación. Básicamente, se trata de reconstruir las expresiones booleanas y asignaciones a partir de la secuencia de tokens:
|
||||||
|
|
||||||
|
* **Contactos:** Cada `_LD_CONTACT Var` se convierte en una condición booleana sobre `Var`. Si el token va seguido de `_POSITIV`, significa que la condición es simplemente `Var` (TRUE cuando la variable es TRUE). Si estuviera negado, la condición sería `NOT Var`. En pseudocódigo ST podemos representar un contacto normalmente abierto como `Var` y uno normalmente cerrado como `NOT Var`.
|
||||||
|
* **Operadores lógicos AND/OR:** Tokens como `_LD_AND` con `_LD_OPERATOR:n` indican combinaciones lógicas. Por ejemplo, si hay dos contactos seguidos de `_LD_AND`, en ST sería una conjunción: `Cond1 AND Cond2`. Si hubiera `_LD_OR`, sería una disyunción: `Cond1 OR Cond2`. Estos operadores reflejan ramas en serie (AND) o en paralelo (OR) en el esquema Ladder. Por ejemplo, `_LD_AND` con 2 operandos se traduce como `ExpresionResultado = (Expr1 AND Expr2)`.
|
||||||
|
* **Asignación a salidas:** El token `_LD_ASSIGN` señala que la expresión lógica formada por los contactos y operadores anteriores ya determina el resultado del rung. En Ladder, ese resultado se envía a una o varias bobinas. En pseudocódigo, esto corresponde a realizar asignaciones a las variables de salida. Si `_OUTPUTS : 1`, hay una sola salida y simplemente pondremos esa variable igual a la expresión booleana resultante. Si hay múltiples salidas (p. ej. dos bobinas en paralelo), cada una recibirá el mismo valor de la expresión lógica. Por ejemplo, si la lógica calculada es `Expr` y hay dos salidas `Out1` y `Out2`, en ST podríamos escribir: `Out1 := Expr; Out2 := Expr;`.
|
||||||
|
* **Bobinas (coils):** Un `_OUTPUT _POSITIV _NO_SET Var` se interpreta como una asignación directa: `Var := ResultadoLogico`. Si la bobina estuviera invertida (`_NEG`), equivaldría a `Var := NOT(ResultadoLogico)`. Si es un coil de *Set*, en Ladder significa que cuando la condición es TRUE se *establece* la variable (la mantiene a 1 hasta otro evento), lo cual en pseudocódigo se modelaría con algo como `IF Resultado THEN Var := TRUE; END_IF` (y análogamente un coil de Reset con `IF Resultado THEN Var := FALSE; END_IF`). No obstante, Ladder maneja set/reset de forma interna, por lo que para documentación suele ser suficiente indicar “(Set)” o “(Reset)” junto a la asignación.
|
||||||
|
* **Rung completo:** En conjunto, cada `_NETWORK` puede traducirse a un bloque *IF/THEN* o a una expresión booleana asignada. Una forma de documentarlo estilo ST es escribir la ecuación booleana de la red. Por ejemplo, considerando el fragmento anterior con dos contactos en serie asignando una bobina `Motor1`, la pseudocódigo podría ser: `Motor1 := A0001 AND A0002;` (suponiendo `A0001` y `A0002` son variables booleanas). Si hubiera contactos en paralelo (OR), se agruparían con paréntesis adecuadamente. Alternativamente, se puede expresarlo como lógica condicional:
|
||||||
|
|
||||||
|
```st
|
||||||
|
IF (A0001 AND A0002) THEN
|
||||||
|
Motor1 := TRUE;
|
||||||
|
ELSE
|
||||||
|
Motor1 := FALSE;
|
||||||
|
END_IF;
|
||||||
|
```
|
||||||
|
|
||||||
|
Ambas formas representan la misma lógica de la red Ladder en un formato textual claro.
|
||||||
|
|
||||||
|
**Notas:** También existen tokens para construcciones especiales. Por ejemplo, `_JUMP <etiqueta>` puede aparecer en `.exp` para reflejar instrucciones de salto (gotos) dentro de Ladder *il* o saltos condicionales (similar a instrucciones en lenguaje IL) – aunque en Ladder puro estándar no son comunes, CoDeSys permitía elementos como `jump`. Otro posible token es `_EN`/`_ENO` para conexiones de habilitación a cajas de función (FB/funciones) insertadas en Ladder. Estos casos avanzados van más allá de simples contactos y bobinas, pero siguen una lógica similar: el `.exp` listará llamados a funciones o saltos con sus parámetros en texto. Si el objetivo es documentación, normalmente se enfoca en la lógica combinacional de contactos y bobinas, que es lo descrito arriba.
|
||||||
|
|
||||||
|
## Herramientas y recursos para la conversión
|
||||||
|
|
||||||
|
Encontrar documentación detallada de este formato no estándar puede ser difícil, pero existen **recursos oficiales y de la comunidad** que ayudan a interpretarlo. Beckhoff no publica abiertamente la gramática completa de `.exp`, pero la información fragmentada en manuales y foros nos da guía. Por ejemplo, un manual de HollySys (un PLC basado en CoDeSys) incluye una sección explicando cada token Ladder (\_LD\_CONTACT, \_LD\_AND, \_OUTPUT, etc.) y cómo corresponden a los elementos gráficos. Aunque esté en chino, confirma la semántica: por ejemplo, `_LD_CONTACT --触点标识... _EXPRESSION --是否置反标识 _POSITIV` significa que `_LD_CONTACT` identifica un contacto y `_POSITIV` indica que **no** está negado. Del mismo modo, muestra `_OUTPUT ... _POSITIV _NO_SET ...` para una bobina normal. Este tipo de documentación no oficial puede servir de referencia de mapeo.
|
||||||
|
|
||||||
|
En cuanto a herramientas automáticas para convertir `.exp` Ladder a código legible o ST, **no se conocen utilidades públicas específicas** enfocadas solo en TwinCAT 2 `.exp`. Sin embargo, hay enfoques posibles:
|
||||||
|
|
||||||
|
* **Uso de TwinCAT 3/PLCopen:** Beckhoff TwinCAT 3 ofrece un **convertidor de formatos TwinCAT 2** integrado. Es posible importar el proyecto o POU exportado de TwinCAT 2 (archivo `.exp` o `.tpy`) a TwinCAT 3 y luego exportarlo a XML PLCopen, que es un formato estándar. El XML PLCopen describirá la lógica Ladder de forma estructurada, más fácil de leer o de procesar con scripts (por ejemplo, extrayendo las ecuaciones lógicas). De hecho, un experto sugiere usar PLCopen XML como vía de intercambio entre plataformas. Esto requeriría tener TwinCAT 3 instalado para la conversión, pero puede ahorrar tiempo si se dispone de muchos rungs.
|
||||||
|
* **Scripting personalizado:** Dado que el `.exp` es texto ASCII, se puede escribir un script (en Python u otro lenguaje) para *parsear* los tokens y regenerar la lógica. La gramática es relativamente simple (como se detalló arriba). Por ejemplo, uno podría leer línea a línea, identificar bloques `_NETWORK ... _OUTPUTS` y construir la expresión booleana intermedia. No encontramos una librería Python ya hecha para esto, pero es viable implementarlo. Algunos usuarios en foros han discutido partes del formato precisamente con la idea de traducirlo; por ejemplo, en un foro chino se intentó “traducir un programa Ladder de Siemens (TIA) a Beckhoff” analizando un `.exp` de TwinCAT, lo que evidencia el interés en tales conversiones. Un proyecto open-source relacionado es *Blark* (un parser de código ST de TwinCAT), que aunque está orientado a Structured Text, demuestra que es posible crear gramáticas para lenguajes PLC en Python. Para Ladder, un desarrollador podría definir reglas similares: contactos -> operandos booleanos, `_LD_AND` -> operador AND, etc., o incluso usar expresiones regulares dado el formato estructurado lineal del `.exp`.
|
||||||
|
* **Recursos comunitarios:** Revisar comunidades de automatización y repositorios GitHub puede dar frutos. Si bien no hallamos una herramienta específica lista para usar, sitios como PLCtalk y Stack Overflow tienen hilos donde se menciona la exportación `.exp`. Por ejemplo, en Stack Overflow se preguntó sobre importar .exp entre distintos entornos CoDeSys, confirmando que `.exp` es un formato Codesys genérico utilizado por múltiples marcas. Esto significa que documentación de **CoDeSys 2.3** también aplica (muchos controladores usaban el mismo formato export). En suma, buscar por “CoDeSys export .exp Ladder” puede arrojar tips de usuarios que hayan hecho ingeniería inversa del formato.
|
||||||
|
|
||||||
|
**Conclusión:** Mediante la combinación de fuentes oficiales (manuales de TwinCAT/Codesys) y no oficiales (ejemplos en foros, manuales de terceros), es posible mapear los tokens `_NETWORK`, `_LD_CONTACT`, `_LD_AND`, `_LD_ASSIGN`, `_OUTPUT`, etc., a construcciones lógicas comprensibles. La clave es reconocer la secuencia: una red Ladder en `.exp` corresponde a una expresión booleana (derivada de contactos y operadores) asignada a una o varias salidas. Documentar esa lógica en pseudocódigo estilo ST – por ejemplo, escribiendo ecuaciones lógicas o condiciones IF/THEN – hará más legible el programa. Con los ejemplos y explicaciones recopilados aquí, se tiene un **guía de referencia** para emprender esa traducción manual o mediante script, facilitando la comprensión de programas Ladder exportados de TwinCAT 2.8.
|
||||||
|
|
||||||
|
**Referencias usadas:** Documentación de Beckhoff TwinCAT 2, fragmentos de manual Hollysys/AutoThink, y ejemplos de código `.exp` discutidos en foros, entre otros. Estas fuentes proporcionan casos prácticos y descripciones claras para respaldar la interpretación de cada token y su equivalente lógico. Por último, la utilización de herramientas modernas (TwinCAT 3, PLCopen) o scripts personalizados son caminos recomendados si se requiere automatizar la conversión de `.exp` Ladder a texto estructurado.
|
|
@ -0,0 +1,293 @@
|
||||||
|
# Guía de Configuración para Scripts Backend
|
||||||
|
|
||||||
|
## Introducción
|
||||||
|
|
||||||
|
Esta guía explica cómo configurar y usar correctamente la función `load_configuration()` en scripts ubicados bajo el directorio `/backend`. La función carga configuraciones desde un archivo `script_config.json` ubicado en el mismo directorio que el script que la llama.
|
||||||
|
|
||||||
|
## Configuración del Path e Importación
|
||||||
|
|
||||||
|
### 1. Configuración estándar del Path
|
||||||
|
|
||||||
|
Para scripts ubicados en subdirectorios bajo `/backend`, usa este patrón estándar:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Configurar el path al directorio raíz del proyecto
|
||||||
|
script_root = os.path.dirname(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
)
|
||||||
|
sys.path.append(script_root)
|
||||||
|
|
||||||
|
# Importar la función de configuración
|
||||||
|
from backend.script_utils import load_configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
**Nota:** El número de `os.path.dirname()` anidados depende de la profundidad del script:
|
||||||
|
- Scripts en `/backend/script_groups/grupo/`: 4 niveles
|
||||||
|
- Scripts en `/backend/`: 2 niveles
|
||||||
|
|
||||||
|
### 2. Importación Correcta
|
||||||
|
|
||||||
|
**✅ Correcto:**
|
||||||
|
```python
|
||||||
|
from backend.script_utils import load_configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
**❌ Incorrecto:**
|
||||||
|
```python
|
||||||
|
from script_utils import load_configuration # No funciona desde subdirectorios
|
||||||
|
```
|
||||||
|
|
||||||
|
## Uso de la Función load_configuration()
|
||||||
|
|
||||||
|
### Implementación Básica
|
||||||
|
|
||||||
|
```python
|
||||||
|
def main():
|
||||||
|
# Cargar configuraciones
|
||||||
|
configs = load_configuration()
|
||||||
|
|
||||||
|
# Obtener el directorio de trabajo
|
||||||
|
working_directory = configs.get("working_directory", "")
|
||||||
|
|
||||||
|
# Acceder a configuraciones por nivel
|
||||||
|
level1_config = configs.get("level1", {})
|
||||||
|
level2_config = configs.get("level2", {})
|
||||||
|
level3_config = configs.get("level3", {})
|
||||||
|
|
||||||
|
# Ejemplo de uso de parámetros específicos con valores por defecto
|
||||||
|
scl_output_dir = level2_config.get("scl_output_dir", "scl_output")
|
||||||
|
xref_output_dir = level2_config.get("xref_output_dir", "xref_output")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Estructura del Archivo script_config.json
|
||||||
|
|
||||||
|
El archivo `script_config.json` debe estar ubicado en el mismo directorio que el script que llama a `load_configuration()`. Estructura recomendada:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"working_directory": "/ruta/al/directorio/de/trabajo",
|
||||||
|
"level1": {
|
||||||
|
"parametro_global_1": "valor1",
|
||||||
|
"parametro_global_2": "valor2"
|
||||||
|
},
|
||||||
|
"level2": {
|
||||||
|
"scl_output_dir": "scl_output",
|
||||||
|
"xref_output_dir": "xref_output",
|
||||||
|
"xref_source_subdir": "source",
|
||||||
|
"aggregated_filename": "full_project_representation.md"
|
||||||
|
},
|
||||||
|
"level3": {
|
||||||
|
"parametro_especifico_1": true,
|
||||||
|
"parametro_especifico_2": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ejemplo Completo de Implementación
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""
|
||||||
|
Script de ejemplo que demuestra el uso completo de load_configuration()
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Configuración del path
|
||||||
|
script_root = os.path.dirname(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
)
|
||||||
|
sys.path.append(script_root)
|
||||||
|
from backend.script_utils import load_configuration
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=== Cargando Configuración ===")
|
||||||
|
|
||||||
|
# Cargar configuraciones
|
||||||
|
configs = load_configuration()
|
||||||
|
|
||||||
|
# Verificar que se cargó correctamente
|
||||||
|
if not configs:
|
||||||
|
print("Error: No se pudo cargar la configuración")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Obtener configuraciones
|
||||||
|
working_directory = configs.get("working_directory", "")
|
||||||
|
level1_config = configs.get("level1", {})
|
||||||
|
level2_config = configs.get("level2", {})
|
||||||
|
level3_config = configs.get("level3", {})
|
||||||
|
|
||||||
|
# Mostrar configuraciones cargadas
|
||||||
|
print(f"Directorio de trabajo: {working_directory}")
|
||||||
|
print("Configuración Nivel 1:", json.dumps(level1_config, indent=2))
|
||||||
|
print("Configuración Nivel 2:", json.dumps(level2_config, indent=2))
|
||||||
|
print("Configuración Nivel 3:", json.dumps(level3_config, indent=2))
|
||||||
|
|
||||||
|
# Ejemplo de uso de parámetros con valores por defecto
|
||||||
|
scl_output_dir = level2_config.get("scl_output_dir", "scl_output")
|
||||||
|
xref_output_dir = level2_config.get("xref_output_dir", "xref_output")
|
||||||
|
|
||||||
|
print(f"Directorio de salida SCL: {scl_output_dir}")
|
||||||
|
print(f"Directorio de salida XREF: {xref_output_dir}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manejo de Errores
|
||||||
|
|
||||||
|
La función `load_configuration()` maneja automáticamente los siguientes casos:
|
||||||
|
|
||||||
|
1. **Archivo no encontrado**: Retorna un diccionario vacío `{}`
|
||||||
|
2. **JSON inválido**: Retorna un diccionario vacío y muestra un mensaje de error
|
||||||
|
3. **Errores de lectura**: Retorna un diccionario vacío y muestra un mensaje de error
|
||||||
|
|
||||||
|
### Verificación de Configuración Válida
|
||||||
|
|
||||||
|
```python
|
||||||
|
configs = load_configuration()
|
||||||
|
|
||||||
|
# Verificar que se cargó correctamente
|
||||||
|
if not configs:
|
||||||
|
print("Advertencia: No se pudo cargar la configuración, usando valores por defecto")
|
||||||
|
working_directory = "."
|
||||||
|
else:
|
||||||
|
working_directory = configs.get("working_directory", ".")
|
||||||
|
|
||||||
|
# Verificar directorio de trabajo
|
||||||
|
if not os.path.exists(working_directory):
|
||||||
|
print(f"Error: El directorio de trabajo no existe: {working_directory}")
|
||||||
|
return
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mejores Prácticas
|
||||||
|
|
||||||
|
1. **Siempre proporciona valores por defecto** al usar `.get()`:
|
||||||
|
```python
|
||||||
|
valor = config.get("clave", "valor_por_defecto")
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Verifica la existencia de directorios críticos**:
|
||||||
|
```python
|
||||||
|
if not os.path.exists(working_directory):
|
||||||
|
print(f"Error: Directorio no encontrado: {working_directory}")
|
||||||
|
return
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Documenta los parámetros esperados** en tu script:
|
||||||
|
```python
|
||||||
|
# Parámetros esperados en level2:
|
||||||
|
# - scl_output_dir: Directorio de salida para archivos SCL
|
||||||
|
# - xref_output_dir: Directorio de salida para referencias cruzadas
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Usa nombres de parámetros consistentes** en todos los scripts del mismo grupo.
|
||||||
|
|
||||||
|
## Definición Técnica de load_configuration()
|
||||||
|
|
||||||
|
```python
|
||||||
|
def load_configuration() -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Load configuration from script_config.json in the current script directory.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict containing configurations with levels 1, 2, 3 and working_directory
|
||||||
|
|
||||||
|
Example usage in scripts:
|
||||||
|
from backend.script_utils import load_configuration
|
||||||
|
|
||||||
|
configs = load_configuration()
|
||||||
|
level1_config = configs.get("level1", {})
|
||||||
|
level2_config = configs.get("level2", {})
|
||||||
|
level3_config = configs.get("level3", {})
|
||||||
|
working_dir = configs.get("working_directory", "")
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
La función utiliza `inspect.stack()` para determinar automáticamente el directorio del script que la llama, asegurando que siempre busque el archivo `script_config.json` en la ubicación correcta.
|
||||||
|
|
||||||
|
## Documentación de Scripts para el Launcher
|
||||||
|
|
||||||
|
El sistema de launcher utiliza archivos JSON para mostrar información sobre los grupos de scripts y scripts individuales en la interfaz web.
|
||||||
|
|
||||||
|
### Archivo description.json (Descripción del Grupo)
|
||||||
|
|
||||||
|
Ubicación: En el directorio raíz del grupo de scripts.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Nombre del Grupo",
|
||||||
|
"description": "Descripción del propósito y funcionalidad del grupo",
|
||||||
|
"version": "1.0",
|
||||||
|
"author": "Nombre del Autor"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Archivo scripts_description.json (Descripción de Scripts)
|
||||||
|
|
||||||
|
Ubicación: En el directorio raíz del grupo de scripts.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nombre_script.py": {
|
||||||
|
"display_name": "Nombre para mostrar en la UI",
|
||||||
|
"short_description": "Descripción breve del script",
|
||||||
|
"long_description": "Descripción detallada con explicación completa de funcionalidad, pasos que ejecuta, y contexto de uso",
|
||||||
|
"hidden": false
|
||||||
|
},
|
||||||
|
"script_interno.py": {
|
||||||
|
"display_name": "Script Interno",
|
||||||
|
"short_description": "Script de uso interno",
|
||||||
|
"long_description": "",
|
||||||
|
"hidden": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Propiedades Importantes
|
||||||
|
|
||||||
|
- **hidden**: `true` oculta el script del launcher (útil para scripts auxiliares)
|
||||||
|
- **display_name**: Nombre amigable que aparece en la interfaz
|
||||||
|
- **short_description**: Se muestra en la lista de scripts
|
||||||
|
- **long_description**: Se muestra al expandir detalles del script
|
||||||
|
|
||||||
|
### Ejemplo Práctico
|
||||||
|
|
||||||
|
Para un grupo "XML Parser to SCL":
|
||||||
|
|
||||||
|
**description.json:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Siemens-Tia : 03 : Procesador de XML LAD-SCL-AWL",
|
||||||
|
"description": "Scripts que procesan archivos XML exportados de TIA, convirtiendo LAD a SCL",
|
||||||
|
"version": "1.0",
|
||||||
|
"author": "Miguel"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**scripts_description.json:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"x0_main.py": {
|
||||||
|
"display_name": "1: Procesar Exportación XML completa",
|
||||||
|
"short_description": "Conversor principal de LAD/FUP XML a SCL",
|
||||||
|
"long_description": "Script orquestador que procesa todos los archivos XML...",
|
||||||
|
"hidden": false
|
||||||
|
},
|
||||||
|
"x1_to_json.py": {
|
||||||
|
"display_name": "x1_to_json",
|
||||||
|
"short_description": "Converter XML interno",
|
||||||
|
"long_description": "",
|
||||||
|
"hidden": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
|
@ -7,9 +7,21 @@ Versión mejorada con SymPy para optimización de expresiones lógicas
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import sympy
|
import sympy
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import glob
|
||||||
from sympy import symbols, And, Or, Not, simplify
|
from sympy import symbols, And, Or, Not, simplify
|
||||||
from sympy.logic.boolalg import to_dnf
|
from sympy.logic.boolalg import to_dnf
|
||||||
|
|
||||||
|
# Configurar el path al directorio raíz del proyecto
|
||||||
|
script_root = os.path.dirname(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
)
|
||||||
|
sys.path.append(script_root)
|
||||||
|
|
||||||
|
# Importar la función de configuración
|
||||||
|
from backend.script_utils import load_configuration
|
||||||
|
|
||||||
class SymbolManager:
|
class SymbolManager:
|
||||||
"""Gestor de símbolos para SymPy"""
|
"""Gestor de símbolos para SymPy"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -116,12 +128,26 @@ class SimpleLadConverter:
|
||||||
def _parse_networks(self, lines):
|
def _parse_networks(self, lines):
|
||||||
"""Parse todas las redes"""
|
"""Parse todas las redes"""
|
||||||
i = 0
|
i = 0
|
||||||
|
expected_networks = 0
|
||||||
|
|
||||||
|
# Buscar cuántas redes se esperan
|
||||||
|
for line in lines:
|
||||||
|
if line.strip().startswith('_NETWORKS :'):
|
||||||
|
expected_networks = int(line.strip().split(':')[1].strip())
|
||||||
|
print(f"Se esperan {expected_networks} redes según el archivo")
|
||||||
|
break
|
||||||
|
|
||||||
while i < len(lines):
|
while i < len(lines):
|
||||||
if lines[i].strip() == '_NETWORK':
|
if lines[i].strip() == '_NETWORK':
|
||||||
self.current_network_id += 1
|
self.current_network_id += 1
|
||||||
|
print(f"Procesando red {self.current_network_id}...")
|
||||||
i = self._parse_network(lines, i)
|
i = self._parse_network(lines, i)
|
||||||
else:
|
else:
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
|
if len(self.networks) != expected_networks:
|
||||||
|
print(f"ADVERTENCIA: Se esperaban {expected_networks} redes pero solo se parsearon {len(self.networks)}")
|
||||||
|
print("Esto puede indicar redes con _EMPTY o estructuras no reconocidas")
|
||||||
|
|
||||||
def _parse_network(self, lines, start_idx):
|
def _parse_network(self, lines, start_idx):
|
||||||
"""Parse una red individual con soporte mejorado para operadores LAD"""
|
"""Parse una red individual con soporte mejorado para operadores LAD"""
|
||||||
|
@ -163,6 +189,10 @@ class SimpleLadConverter:
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
self.networks.append(network)
|
self.networks.append(network)
|
||||||
|
print(f" Red {network['id']} agregada. Total redes: {len(self.networks)}")
|
||||||
|
if network['logic']:
|
||||||
|
print(f" Con lógica: {network['logic']['type']} - {network['logic'].get('name', 'Sin nombre')}")
|
||||||
|
print(f" Target: '{network['target']}'")
|
||||||
return i
|
return i
|
||||||
|
|
||||||
def _parse_lad_expression(self, lines, start_idx):
|
def _parse_lad_expression(self, lines, start_idx):
|
||||||
|
@ -180,6 +210,13 @@ class SimpleLadConverter:
|
||||||
return self._parse_contact(lines, i + 1)
|
return self._parse_contact(lines, i + 1)
|
||||||
elif line.startswith('_FUNCTIONBLOCK'):
|
elif line.startswith('_FUNCTIONBLOCK'):
|
||||||
return self._parse_function_block(lines, i)
|
return self._parse_function_block(lines, i)
|
||||||
|
elif line == '_EMPTY':
|
||||||
|
# Red vacía que puede contener funciones después
|
||||||
|
print(f" Encontrada _EMPTY dentro de _LD_ASSIGN en línea {i}")
|
||||||
|
i += 1
|
||||||
|
# Buscar funciones en las siguientes líneas
|
||||||
|
i, empty_logic = self._parse_empty_network(lines, i)
|
||||||
|
return i, empty_logic
|
||||||
elif line.startswith('_OUTPUT') or line == 'ENABLELIST : 0':
|
elif line.startswith('_OUTPUT') or line == 'ENABLELIST : 0':
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
@ -252,28 +289,69 @@ class SimpleLadConverter:
|
||||||
return i, {'type': 'CONTACT', 'name': contact_name, 'negated': negated}
|
return i, {'type': 'CONTACT', 'name': contact_name, 'negated': negated}
|
||||||
|
|
||||||
def _parse_function_block(self, lines, start_idx):
|
def _parse_function_block(self, lines, start_idx):
|
||||||
"""Parse un bloque de función"""
|
"""Parse un bloque de función o llamada a ACTION"""
|
||||||
i = start_idx + 1
|
i = start_idx + 1
|
||||||
fb_name = ""
|
fb_name = ""
|
||||||
inputs = []
|
inputs = []
|
||||||
|
action_call = None
|
||||||
|
max_iterations = 50 # Protección contra bucles infinitos
|
||||||
|
iterations = 0
|
||||||
|
|
||||||
if i < len(lines):
|
if i < len(lines):
|
||||||
fb_name = lines[i].strip()
|
fb_name = lines[i].strip()
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
# Parse inputs del function block
|
# Verificar si es una llamada a ACTION (patrón ??? seguido de namespace.actionname)
|
||||||
while i < len(lines) and not lines[i].strip().startswith('_OUTPUT'):
|
if fb_name == "???":
|
||||||
|
# Buscar el nombre completo de la ACTION más adelante
|
||||||
|
action_call = self._find_action_call_name(lines, i)
|
||||||
|
if action_call:
|
||||||
|
print(f" Detectada llamada a ACTION: {action_call}")
|
||||||
|
return i, {'type': 'ACTION_CALL', 'name': action_call, 'inputs': []}
|
||||||
|
|
||||||
|
# Parse inputs del function block normal
|
||||||
|
while (i < len(lines) and
|
||||||
|
not lines[i].strip().startswith('_OUTPUT') and
|
||||||
|
iterations < max_iterations):
|
||||||
|
|
||||||
line = lines[i].strip()
|
line = lines[i].strip()
|
||||||
|
iterations += 1
|
||||||
|
|
||||||
if line.startswith('_OPERAND'):
|
if line.startswith('_OPERAND'):
|
||||||
i += 2 # Saltar _EXPRESSION
|
i += 2 # Saltar _EXPRESSION
|
||||||
if i < len(lines):
|
if i < len(lines):
|
||||||
inputs.append(lines[i].strip())
|
inputs.append(lines[i].strip())
|
||||||
i += 1
|
i += 1
|
||||||
|
elif line and not line.startswith('_'):
|
||||||
|
# Podría ser el nombre de la ACTION
|
||||||
|
if '.' in line and action_call is None:
|
||||||
|
action_call = line
|
||||||
|
print(f" Detectada llamada a ACTION: {action_call}")
|
||||||
|
return i, {'type': 'ACTION_CALL', 'name': action_call, 'inputs': inputs}
|
||||||
|
elif line.startswith('ENABLELIST'):
|
||||||
|
# Fin del bloque
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
|
# Salida de emergencia del bucle
|
||||||
|
if iterations >= max_iterations:
|
||||||
|
print(f" ADVERTENCIA: Bucle infinito evitado en function block en línea {start_idx}")
|
||||||
|
break
|
||||||
|
|
||||||
return i, {'type': 'FUNCTION_BLOCK', 'name': fb_name, 'inputs': inputs}
|
return i, {'type': 'FUNCTION_BLOCK', 'name': fb_name, 'inputs': inputs}
|
||||||
|
|
||||||
|
def _find_action_call_name(self, lines, start_idx):
|
||||||
|
"""Buscar el nombre completo de la ACTION después de ???"""
|
||||||
|
i = start_idx
|
||||||
|
while i < len(lines) and i < start_idx + 10: # Buscar en las próximas 10 líneas
|
||||||
|
line = lines[i].strip()
|
||||||
|
# Buscar patrón namespace.actionname
|
||||||
|
if '.' in line and not line.startswith('_') and 'ENABLELIST' not in line:
|
||||||
|
return line
|
||||||
|
i += 1
|
||||||
|
return None
|
||||||
|
|
||||||
def _parse_comment(self, lines, start_idx):
|
def _parse_comment(self, lines, start_idx):
|
||||||
"""Parse comentario"""
|
"""Parse comentario"""
|
||||||
i = start_idx + 1
|
i = start_idx + 1
|
||||||
|
@ -289,6 +367,164 @@ class SimpleLadConverter:
|
||||||
|
|
||||||
return i + 1, ' '.join(comment_lines)
|
return i + 1, ' '.join(comment_lines)
|
||||||
|
|
||||||
|
def _parse_empty_network(self, lines, start_idx):
|
||||||
|
"""Parse una red que empieza con _EMPTY y puede contener funciones"""
|
||||||
|
i = start_idx
|
||||||
|
max_iterations = 20
|
||||||
|
iterations = 0
|
||||||
|
function_found = None
|
||||||
|
target_found = None
|
||||||
|
|
||||||
|
print(f" Entrando a _parse_empty_network desde línea {start_idx}")
|
||||||
|
|
||||||
|
while (i < len(lines) and
|
||||||
|
iterations < max_iterations and
|
||||||
|
not lines[i].strip().startswith('_OUTPUT') and
|
||||||
|
not lines[i].strip() == '_NETWORK'):
|
||||||
|
|
||||||
|
line = lines[i].strip()
|
||||||
|
iterations += 1
|
||||||
|
print(f" Línea {i}: '{line}'")
|
||||||
|
|
||||||
|
if line.startswith('_FUNCTIONBLOCK'):
|
||||||
|
# Encontrada llamada a función/ACTION
|
||||||
|
print(f" ENCONTRADO _FUNCTIONBLOCK en línea {i}")
|
||||||
|
i, logic = self._parse_function_block(lines, i)
|
||||||
|
function_found = logic
|
||||||
|
elif line.startswith('_FUNCTION'):
|
||||||
|
# Otra variante de función
|
||||||
|
print(f" ENCONTRADO _FUNCTION en línea {i}")
|
||||||
|
i += 1
|
||||||
|
# Buscar el nombre de la función
|
||||||
|
while i < len(lines) and i < start_idx + 10:
|
||||||
|
func_line = lines[i].strip()
|
||||||
|
print(f" Buscando nombre función línea {i}: '{func_line}'")
|
||||||
|
if (func_line and
|
||||||
|
not func_line.startswith('_') and
|
||||||
|
'ENABLELIST' not in func_line):
|
||||||
|
print(f" ENCONTRADO nombre función: {func_line}")
|
||||||
|
function_found = {'type': 'FUNCTION_CALL', 'name': func_line, 'inputs': []}
|
||||||
|
break
|
||||||
|
i += 1
|
||||||
|
elif line.startswith('ENABLELIST'):
|
||||||
|
print(f" Encontrado ENABLELIST, continuando búsqueda...")
|
||||||
|
# No terminar aquí, continuar buscando _ASSIGN
|
||||||
|
i += 1
|
||||||
|
elif line.startswith('_ASSIGN'):
|
||||||
|
print(f" ENCONTRADO _ASSIGN en línea {i}")
|
||||||
|
# Buscar función dentro de _ASSIGN
|
||||||
|
i += 1
|
||||||
|
i, assign_logic = self._parse_assign_section(lines, i)
|
||||||
|
if assign_logic:
|
||||||
|
function_found = assign_logic
|
||||||
|
elif line.startswith('_OUTPUT'):
|
||||||
|
# Buscar el target después de _OUTPUT
|
||||||
|
print(f" ENCONTRADO _OUTPUT, buscando target...")
|
||||||
|
i += 1
|
||||||
|
while i < len(lines) and lines[i].strip().startswith('_'):
|
||||||
|
i += 1
|
||||||
|
if i < len(lines) and lines[i].strip() and 'ENABLELIST' not in lines[i]:
|
||||||
|
target_found = lines[i].strip()
|
||||||
|
print(f" ENCONTRADO target: {target_found}")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# Si encontramos una función, crear red independiente
|
||||||
|
if function_found and target_found:
|
||||||
|
print(f" Creando red independiente para función con target: {target_found}")
|
||||||
|
self._create_function_network(function_found, target_found)
|
||||||
|
return i, function_found
|
||||||
|
elif function_found:
|
||||||
|
print(f" Función encontrada pero sin target específico")
|
||||||
|
# Crear target por defecto basado en el tipo de función
|
||||||
|
if function_found['type'] == 'ACTION_CALL':
|
||||||
|
default_target = 'mDummy' # Variable dummy típica para ACTION calls
|
||||||
|
elif function_found['type'] == 'FUNCTION_CALL':
|
||||||
|
# Para funciones como ProductLiterInTank, usar la variable global correspondiente
|
||||||
|
if function_found['name'] == 'gProductTankLevel':
|
||||||
|
default_target = 'gTankProdAmount' # Variable que se asigna según el contexto
|
||||||
|
else:
|
||||||
|
default_target = 'mDummy'
|
||||||
|
else:
|
||||||
|
# Caso por defecto para otros tipos de función (FUNCTION_BLOCK, etc.)
|
||||||
|
default_target = 'mDummy'
|
||||||
|
|
||||||
|
print(f" Usando target por defecto: {default_target}")
|
||||||
|
self._create_function_network(function_found, default_target)
|
||||||
|
return i, function_found
|
||||||
|
|
||||||
|
print(f" _parse_empty_network terminó sin encontrar función")
|
||||||
|
return i, None
|
||||||
|
|
||||||
|
def _parse_assign_section(self, lines, start_idx):
|
||||||
|
"""Parse una sección _ASSIGN que puede contener funciones"""
|
||||||
|
i = start_idx
|
||||||
|
max_iterations = 15
|
||||||
|
iterations = 0
|
||||||
|
|
||||||
|
print(f" Entrando a _parse_assign_section desde línea {start_idx}")
|
||||||
|
|
||||||
|
while (i < len(lines) and
|
||||||
|
iterations < max_iterations and
|
||||||
|
not lines[i].strip() == '_NETWORK' and
|
||||||
|
not lines[i].strip() == 'ENABLELIST_END'):
|
||||||
|
|
||||||
|
line = lines[i].strip()
|
||||||
|
iterations += 1
|
||||||
|
print(f" Línea {i}: '{line}'")
|
||||||
|
|
||||||
|
if line.startswith('_FUNCTIONBLOCK'):
|
||||||
|
print(f" ENCONTRADO _FUNCTIONBLOCK en _ASSIGN: línea {i}")
|
||||||
|
i, logic = self._parse_function_block(lines, i)
|
||||||
|
return i, logic
|
||||||
|
elif line.startswith('_FUNCTION'):
|
||||||
|
print(f" ENCONTRADO _FUNCTION en _ASSIGN: línea {i}")
|
||||||
|
# Buscar el nombre de la función en las siguientes líneas
|
||||||
|
func_name = None
|
||||||
|
j = i + 1
|
||||||
|
while j < len(lines) and j < i + 8:
|
||||||
|
func_line = lines[j].strip()
|
||||||
|
print(f" Buscando nombre función línea {j}: '{func_line}'")
|
||||||
|
if (func_line and
|
||||||
|
not func_line.startswith('_') and
|
||||||
|
'ENABLELIST' not in func_line and
|
||||||
|
':' not in func_line):
|
||||||
|
func_name = func_line
|
||||||
|
print(f" ENCONTRADO nombre función: {func_name}")
|
||||||
|
break
|
||||||
|
j += 1
|
||||||
|
|
||||||
|
if func_name:
|
||||||
|
return j, {'type': 'FUNCTION_CALL', 'name': func_name, 'inputs': []}
|
||||||
|
else:
|
||||||
|
i += 1
|
||||||
|
elif line == 'ENABLELIST_END':
|
||||||
|
print(f" Encontrado ENABLELIST_END, terminando")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
print(f" _parse_assign_section terminó sin encontrar función")
|
||||||
|
return i, None
|
||||||
|
|
||||||
|
def _create_function_network(self, function_logic, target_name):
|
||||||
|
"""Crear una red independiente para una función o ACTION call"""
|
||||||
|
self.current_network_id += 1
|
||||||
|
|
||||||
|
function_network = {
|
||||||
|
'id': self.current_network_id,
|
||||||
|
'comment': f'Llamada a función: {function_logic.get("name", "unknown")}',
|
||||||
|
'logic': function_logic,
|
||||||
|
'target': target_name,
|
||||||
|
'function_blocks': []
|
||||||
|
}
|
||||||
|
|
||||||
|
self.networks.append(function_network)
|
||||||
|
print(f" Red de función {function_network['id']} creada para {function_logic['type']}: {function_logic.get('name', 'Sin nombre')}")
|
||||||
|
print(f" Target: '{target_name}'")
|
||||||
|
return function_network
|
||||||
|
|
||||||
def convert_to_structured(self):
|
def convert_to_structured(self):
|
||||||
"""Convertir a código SCL completo con variables y ACTIONs"""
|
"""Convertir a código SCL completo con variables y ACTIONs"""
|
||||||
output = []
|
output = []
|
||||||
|
@ -319,29 +555,42 @@ class SimpleLadConverter:
|
||||||
output.append(f" // {network['comment']}")
|
output.append(f" // {network['comment']}")
|
||||||
|
|
||||||
if network['logic'] and network['target']:
|
if network['logic'] and network['target']:
|
||||||
# Usar expresión DNF optimizada si está disponible
|
# Verificar si es una llamada a ACTION sin condiciones
|
||||||
if network['id'] in self.sympy_expressions:
|
if (network['logic']['type'] == 'ACTION_CALL' and
|
||||||
sympy_expr = self.sympy_expressions[network['id']]
|
network['target'] in ['mDummy', 'EN_Out']): # Variables dummy típicas
|
||||||
condition_str = self._format_dnf_for_lad(sympy_expr)
|
# Es una llamada a ACTION incondicional
|
||||||
output.append(f" // Optimizada con SymPy DNF")
|
action_call = self._convert_logic_to_string(network['logic'])
|
||||||
|
output.append(f" {action_call};")
|
||||||
|
output.append(f" {network['target']} := TRUE; // ACTION ejecutada")
|
||||||
|
elif (network['logic']['type'] == 'FUNCTION_CALL' and
|
||||||
|
network['target'] in ['mDummy', 'EN_Out', 'gTankProdAmount']): # Variables típicas
|
||||||
|
# Es una llamada a FUNCTION incondicional
|
||||||
|
function_call = self._convert_logic_to_string(network['logic'])
|
||||||
|
output.append(f" {network['target']} := {function_call};")
|
||||||
else:
|
else:
|
||||||
# Fallback al método original
|
# Usar expresión DNF optimizada si está disponible
|
||||||
condition_str = self._convert_logic_to_string(network['logic'])
|
if network['id'] in self.sympy_expressions:
|
||||||
output.append(f" // Sin optimización SymPy")
|
sympy_expr = self.sympy_expressions[network['id']]
|
||||||
|
condition_str = self._format_dnf_for_lad(sympy_expr)
|
||||||
if condition_str:
|
output.append(f" // Optimizada con SymPy DNF")
|
||||||
# Si hay saltos de línea en la condición (múltiples términos OR)
|
|
||||||
if '\n' in condition_str:
|
|
||||||
output.append(f" IF {condition_str} THEN")
|
|
||||||
else:
|
else:
|
||||||
output.append(f" IF {condition_str} THEN")
|
# Fallback al método original
|
||||||
|
condition_str = self._convert_logic_to_string(network['logic'])
|
||||||
|
output.append(f" // Sin optimización SymPy")
|
||||||
|
|
||||||
output.append(f" {network['target']} := TRUE;")
|
if condition_str:
|
||||||
output.append(" ELSE")
|
# Si hay saltos de línea en la condición (múltiples términos OR)
|
||||||
output.append(f" {network['target']} := FALSE;")
|
if '\n' in condition_str:
|
||||||
output.append(" END_IF;")
|
output.append(f" IF {condition_str} THEN")
|
||||||
else:
|
else:
|
||||||
output.append(f" {network['target']} := TRUE; // Logic no reconocida")
|
output.append(f" IF {condition_str} THEN")
|
||||||
|
|
||||||
|
output.append(f" {network['target']} := TRUE;")
|
||||||
|
output.append(" ELSE")
|
||||||
|
output.append(f" {network['target']} := FALSE;")
|
||||||
|
output.append(" END_IF;")
|
||||||
|
else:
|
||||||
|
output.append(f" {network['target']} := TRUE; // Logic no reconocida")
|
||||||
|
|
||||||
output.append("")
|
output.append("")
|
||||||
|
|
||||||
|
@ -440,6 +689,12 @@ class SimpleLadConverter:
|
||||||
inputs_str = ", ".join(logic['inputs']) if logic['inputs'] else ""
|
inputs_str = ", ".join(logic['inputs']) if logic['inputs'] else ""
|
||||||
return f"{logic['name']}({inputs_str})"
|
return f"{logic['name']}({inputs_str})"
|
||||||
|
|
||||||
|
elif logic['type'] == 'ACTION_CALL':
|
||||||
|
return f"CALL {logic['name']}()"
|
||||||
|
|
||||||
|
elif logic['type'] == 'FUNCTION_CALL':
|
||||||
|
return f"{logic['name']}()"
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def save_to_file(self, output_path):
|
def save_to_file(self, output_path):
|
||||||
|
@ -449,7 +704,6 @@ class SimpleLadConverter:
|
||||||
with open(output_path, 'w', encoding='utf-8') as f:
|
with open(output_path, 'w', encoding='utf-8') as f:
|
||||||
f.write(structured_code)
|
f.write(structured_code)
|
||||||
|
|
||||||
print(f"Código guardado en: {output_path}")
|
|
||||||
return structured_code
|
return structured_code
|
||||||
|
|
||||||
def print_debug_info(self):
|
def print_debug_info(self):
|
||||||
|
@ -495,6 +749,12 @@ class SimpleLadConverter:
|
||||||
elif logic['type'] == 'FUNCTION_BLOCK':
|
elif logic['type'] == 'FUNCTION_BLOCK':
|
||||||
return f"{prefix}FUNCTION_BLOCK: {logic['name']} inputs: {logic['inputs']}"
|
return f"{prefix}FUNCTION_BLOCK: {logic['name']} inputs: {logic['inputs']}"
|
||||||
|
|
||||||
|
elif logic['type'] == 'ACTION_CALL':
|
||||||
|
return f"{prefix}ACTION_CALL: {logic['name']}"
|
||||||
|
|
||||||
|
elif logic['type'] == 'FUNCTION_CALL':
|
||||||
|
return f"{prefix}FUNCTION_CALL: {logic['name']}"
|
||||||
|
|
||||||
return f"{prefix}UNKNOWN: {logic}"
|
return f"{prefix}UNKNOWN: {logic}"
|
||||||
|
|
||||||
def _logic_to_sympy(self, logic):
|
def _logic_to_sympy(self, logic):
|
||||||
|
@ -542,6 +802,11 @@ class SimpleLadConverter:
|
||||||
fb_name = f"{logic['name']}({', '.join(logic['inputs'])})"
|
fb_name = f"{logic['name']}({', '.join(logic['inputs'])})"
|
||||||
return self.symbol_manager.get_symbol(fb_name)
|
return self.symbol_manager.get_symbol(fb_name)
|
||||||
|
|
||||||
|
elif logic['type'] == 'ACTION_CALL':
|
||||||
|
# Para llamadas a ACTION, creamos un símbolo especial
|
||||||
|
action_name = f"CALL_{logic['name'].replace('.', '_')}"
|
||||||
|
return self.symbol_manager.get_symbol(action_name)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _sympy_to_structured_string(self, sympy_expr):
|
def _sympy_to_structured_string(self, sympy_expr):
|
||||||
|
@ -589,16 +854,25 @@ class SimpleLadConverter:
|
||||||
simplified = simplify(sympy_expr)
|
simplified = simplify(sympy_expr)
|
||||||
print(f" Simplificada: {simplified}")
|
print(f" Simplificada: {simplified}")
|
||||||
|
|
||||||
# SIEMPRE convertir a DNF para LAD (forma natural: (AND) OR (AND) OR (AND))
|
# Verificar complejidad antes de DNF
|
||||||
dnf_expr = to_dnf(simplified)
|
num_symbols = len(simplified.free_symbols)
|
||||||
print(f" DNF (forma LAD preferida): {dnf_expr}")
|
complexity = self._estimate_expression_complexity(simplified)
|
||||||
|
|
||||||
# Post-procesar para eliminar contradicciones
|
if num_symbols > 12 or complexity > 800:
|
||||||
final_expr = self._post_process_expression(dnf_expr)
|
print(f" ADVERTENCIA: Expresión muy compleja ({num_symbols} símbolos, complejidad {complexity})")
|
||||||
|
print(f" Saltando conversión DNF por rendimiento - usando simplificación básica")
|
||||||
# Verificar si el post-procesamiento cambió algo
|
final_expr = simplified
|
||||||
if str(final_expr) != str(dnf_expr):
|
else:
|
||||||
print(f" Post-procesada: {final_expr}")
|
# SIEMPRE convertir a DNF para LAD (forma natural: (AND) OR (AND) OR (AND))
|
||||||
|
dnf_expr = to_dnf(simplified)
|
||||||
|
print(f" DNF (forma LAD preferida): {dnf_expr}")
|
||||||
|
|
||||||
|
# Post-procesar para eliminar contradicciones
|
||||||
|
final_expr = self._post_process_expression(dnf_expr)
|
||||||
|
|
||||||
|
# Verificar si el post-procesamiento cambió algo
|
||||||
|
if str(final_expr) != str(dnf_expr):
|
||||||
|
print(f" Post-procesada: {final_expr}")
|
||||||
|
|
||||||
self.sympy_expressions[network['id']] = final_expr
|
self.sympy_expressions[network['id']] = final_expr
|
||||||
|
|
||||||
|
@ -670,6 +944,47 @@ class SimpleLadConverter:
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _estimate_expression_complexity(self, expr):
|
||||||
|
"""Estimar la complejidad de una expresión para evitar explosión exponencial"""
|
||||||
|
try:
|
||||||
|
# Contar operadores AND/OR anidados
|
||||||
|
complexity = 0
|
||||||
|
expr_str = str(expr)
|
||||||
|
|
||||||
|
# Contar número de operadores
|
||||||
|
and_count = expr_str.count('&')
|
||||||
|
or_count = expr_str.count('|')
|
||||||
|
not_count = expr_str.count('~')
|
||||||
|
|
||||||
|
complexity += and_count * 2 # AND
|
||||||
|
complexity += or_count * 4 # OR (más costoso en DNF)
|
||||||
|
complexity += not_count * 1 # NOT
|
||||||
|
|
||||||
|
# Penalizar anidamiento profundo
|
||||||
|
depth = expr_str.count('(')
|
||||||
|
complexity += depth * 6
|
||||||
|
|
||||||
|
# Número de símbolos únicos
|
||||||
|
num_symbols = len(expr.free_symbols)
|
||||||
|
complexity += num_symbols * 15
|
||||||
|
|
||||||
|
# Detectar patrones problemáticos específicos
|
||||||
|
# CNF con muchos términos (patrón típico que causa explosión)
|
||||||
|
if and_count > 10 and or_count > 15:
|
||||||
|
complexity += 2000 # Penalización severa para CNF complejas
|
||||||
|
|
||||||
|
# Expresiones con muchos términos negativos (difíciles para DNF)
|
||||||
|
if not_count > 8:
|
||||||
|
complexity += 500
|
||||||
|
|
||||||
|
# Longitud total como indicador de complejidad
|
||||||
|
if len(expr_str) > 1000:
|
||||||
|
complexity += len(expr_str) // 2
|
||||||
|
|
||||||
|
return complexity
|
||||||
|
except:
|
||||||
|
return 999999 # Asumir muy complejo si hay error
|
||||||
|
|
||||||
def _post_process_expression(self, expr):
|
def _post_process_expression(self, expr):
|
||||||
"""Post-procesar expresión para eliminar contradicciones obvias"""
|
"""Post-procesar expresión para eliminar contradicciones obvias"""
|
||||||
try:
|
try:
|
||||||
|
@ -787,47 +1102,147 @@ class SimpleLadConverter:
|
||||||
print(f"Total ACTIONs: {len(self.actions)}")
|
print(f"Total ACTIONs: {len(self.actions)}")
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Función principal"""
|
"""Función principal - Convierte todos los archivos .EXP a .SCL"""
|
||||||
converter = SimpleLadConverter()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print("=== Convertidor LAD a SCL con SymPy ===")
|
print("=== Convertidor Masivo LAD a SCL con SymPy ===")
|
||||||
|
|
||||||
# Por ahora probar con SYRUPROOMCTRL que tiene variables y ACTIONs
|
# Cargar configuración
|
||||||
file_path = ".example/SYRUPROOMCTRL.EXP"
|
configs = load_configuration()
|
||||||
output_name = "SYRUPROOMCTRL_scl"
|
|
||||||
|
|
||||||
print(f"Parseando archivo {file_path}...")
|
# Verificar que se cargó correctamente
|
||||||
|
if not configs:
|
||||||
|
print("Advertencia: No se pudo cargar la configuración, usando valores por defecto")
|
||||||
|
working_directory = "./"
|
||||||
|
scl_output_dir = "scl"
|
||||||
|
debug_mode = True
|
||||||
|
show_optimizations = True
|
||||||
|
show_generated_code = False
|
||||||
|
max_display_lines = 50
|
||||||
|
force_regenerate = False
|
||||||
|
else:
|
||||||
|
# Obtener configuraciones
|
||||||
|
working_directory = configs.get("working_directory", "./")
|
||||||
|
level1_config = configs.get("level1", {})
|
||||||
|
level2_config = configs.get("level2", {})
|
||||||
|
level3_config = configs.get("level3", {})
|
||||||
|
|
||||||
|
# Parámetros de configuración
|
||||||
|
debug_mode = level1_config.get("debug_mode", True)
|
||||||
|
show_optimizations = level1_config.get("show_optimizations", True)
|
||||||
|
scl_output_dir = level2_config.get("scl_output_dir", "scl")
|
||||||
|
backup_existing = level2_config.get("backup_existing", True)
|
||||||
|
show_generated_code = level2_config.get("show_generated_code", False)
|
||||||
|
max_display_lines = level2_config.get("max_display_lines", 50)
|
||||||
|
sympy_optimization = level3_config.get("sympy_optimization", True)
|
||||||
|
group_analysis = level3_config.get("group_analysis", True)
|
||||||
|
force_regenerate = level2_config.get("force_regenerate", False) # Nueva opción
|
||||||
|
|
||||||
converter.parse_file(file_path)
|
# Verificar directorio de trabajo
|
||||||
|
if not os.path.exists(working_directory):
|
||||||
|
print(f"Error: El directorio de trabajo no existe: {working_directory}")
|
||||||
|
return
|
||||||
|
|
||||||
print(f"Redes encontradas: {len(converter.networks)}")
|
# Crear directorio de salida SCL
|
||||||
print(f"Secciones de variables: {list(converter.var_sections.keys())}")
|
full_scl_path = os.path.join(working_directory, scl_output_dir)
|
||||||
print(f"ACTIONs encontradas: {list(converter.actions.keys())}")
|
if not os.path.exists(full_scl_path):
|
||||||
|
os.makedirs(full_scl_path)
|
||||||
|
print(f"Directorio creado: {full_scl_path}")
|
||||||
|
|
||||||
# Mostrar información de debug
|
# Buscar todos los archivos .EXP
|
||||||
converter.print_debug_info()
|
exp_pattern = os.path.join(working_directory, "*.EXP")
|
||||||
|
exp_files = glob.glob(exp_pattern)
|
||||||
|
|
||||||
# NUEVO: Optimizar expresiones con SymPy
|
if not exp_files:
|
||||||
converter.optimize_expressions()
|
print(f"No se encontraron archivos .EXP en: {working_directory}")
|
||||||
|
return
|
||||||
|
|
||||||
# NUEVO: Analizar agrupación de condiciones
|
print(f"Encontrados {len(exp_files)} archivos .EXP en: {working_directory}")
|
||||||
converter.group_common_conditions()
|
print(f"Directorio de salida SCL: {full_scl_path}")
|
||||||
|
print()
|
||||||
|
|
||||||
# Convertir y guardar
|
# Procesar cada archivo
|
||||||
print("\nGenerando código SCL completo...")
|
successful_conversions = 0
|
||||||
structured_code = converter.save_to_file(f"{output_name}.txt")
|
failed_conversions = 0
|
||||||
|
|
||||||
# Mostrar el código generado
|
for exp_file in exp_files:
|
||||||
lines = structured_code.split('\n')
|
filename = os.path.basename(exp_file)
|
||||||
print(f"\nCódigo SCL generado ({len(lines)} líneas):")
|
base_name = os.path.splitext(filename)[0]
|
||||||
for i, line in enumerate(lines):
|
scl_filename = f"{base_name}.scl"
|
||||||
print(f"{i+1:3d}: {line}")
|
scl_output_path = os.path.join(full_scl_path, scl_filename)
|
||||||
|
|
||||||
|
# Verificar si ya existe el archivo SCL (exportación progresiva)
|
||||||
|
if os.path.exists(scl_output_path) and not force_regenerate:
|
||||||
|
print(f"{'='*60}")
|
||||||
|
print(f"SALTANDO: {filename} - Ya existe {scl_filename}")
|
||||||
|
print(f" (usa force_regenerate: true en configuración para forzar regeneración)")
|
||||||
|
successful_conversions += 1 # Contar como exitoso
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"{'='*60}")
|
||||||
|
print(f"Procesando: {filename}")
|
||||||
|
print(f"Salida: {scl_filename}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Crear nuevo convertidor para cada archivo
|
||||||
|
converter = SimpleLadConverter()
|
||||||
|
|
||||||
|
# Parsear archivo
|
||||||
|
converter.parse_file(exp_file)
|
||||||
|
|
||||||
|
print(f" ✓ Redes encontradas: {len(converter.networks)}")
|
||||||
|
print(f" ✓ Secciones de variables: {list(converter.var_sections.keys())}")
|
||||||
|
print(f" ✓ ACTIONs encontradas: {list(converter.actions.keys())}")
|
||||||
|
|
||||||
|
# Mostrar información de debug si está habilitado
|
||||||
|
if debug_mode:
|
||||||
|
converter.print_debug_info()
|
||||||
|
|
||||||
|
# Optimizar expresiones con SymPy si está habilitado
|
||||||
|
if sympy_optimization and show_optimizations:
|
||||||
|
converter.optimize_expressions()
|
||||||
|
|
||||||
|
# Analizar agrupación de condiciones si está habilitado
|
||||||
|
if group_analysis and show_optimizations:
|
||||||
|
converter.group_common_conditions()
|
||||||
|
|
||||||
|
# Convertir y guardar
|
||||||
|
print(f" Generando código SCL...")
|
||||||
|
structured_code = converter.save_to_file(scl_output_path)
|
||||||
|
|
||||||
|
# Mostrar parte del código generado si está habilitado
|
||||||
|
if show_generated_code:
|
||||||
|
lines = structured_code.split('\n')
|
||||||
|
display_lines = min(max_display_lines, len(lines))
|
||||||
|
print(f" Código SCL generado ({len(lines)} líneas, mostrando {display_lines}):")
|
||||||
|
for i in range(display_lines):
|
||||||
|
print(f" {i+1:3d}: {lines[i]}")
|
||||||
|
if len(lines) > display_lines:
|
||||||
|
print(f" ... ({len(lines) - display_lines} líneas más)")
|
||||||
|
|
||||||
|
print(f" ✓ Guardado en: {scl_output_path}")
|
||||||
|
successful_conversions += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ✗ Error procesando {filename}: {e}")
|
||||||
|
if debug_mode:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
failed_conversions += 1
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
print(f"\n✓ Conversión SCL completada!")
|
# Resumen final
|
||||||
|
print(f"{'='*60}")
|
||||||
|
print(f"RESUMEN DE CONVERSIÓN:")
|
||||||
|
print(f" ✓ Exitosas: {successful_conversions}")
|
||||||
|
print(f" ✗ Fallidas: {failed_conversions}")
|
||||||
|
print(f" 📁 Directorio salida: {full_scl_path}")
|
||||||
|
|
||||||
|
if successful_conversions > 0:
|
||||||
|
print(f"\n✓ Conversión masiva completada!")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error: {e}")
|
print(f"Error general: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
|
@ -1,585 +0,0 @@
|
||||||
"""
|
|
||||||
LadderToSCL - Conversor de Siemens LAD/FUP XML a SCL
|
|
||||||
|
|
||||||
Este script convierte un archivo JSON simplificado (resultado de un análisis de un XML de Siemens) a un
|
|
||||||
JSON enriquecido con lógica SCL. Se enfoca en la lógica de programación y la agrupación de instrucciones IF.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import json
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import copy
|
|
||||||
import traceback
|
|
||||||
import re
|
|
||||||
import importlib
|
|
||||||
import sys
|
|
||||||
import sympy
|
|
||||||
script_root = os.path.dirname(
|
|
||||||
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
|
||||||
)
|
|
||||||
sys.path.append(script_root)
|
|
||||||
from backend.script_utils import load_configuration
|
|
||||||
|
|
||||||
# Import necessary components from processors directory
|
|
||||||
from processors.processor_utils import format_variable_name, sympy_expr_to_scl
|
|
||||||
from processors.symbol_manager import SymbolManager
|
|
||||||
|
|
||||||
# --- Constantes y Configuración ---
|
|
||||||
SCL_SUFFIX = "_sympy_processed"
|
|
||||||
GROUPED_COMMENT = "// Logic included in grouped IF"
|
|
||||||
SIMPLIFIED_IF_COMMENT = "// Simplified IF condition by script"
|
|
||||||
|
|
||||||
# Global data dictionary
|
|
||||||
data = {}
|
|
||||||
|
|
||||||
|
|
||||||
# --- (process_group_ifs y load_processors SIN CAMBIOS) ---
|
|
||||||
def process_group_ifs(instruction, network_id, sympy_map, symbol_manager, data):
|
|
||||||
instr_uid = instruction["instruction_uid"]
|
|
||||||
instr_type_original = (
|
|
||||||
instruction.get("type", "").replace(SCL_SUFFIX, "").replace("_error", "")
|
|
||||||
)
|
|
||||||
made_change = False
|
|
||||||
if (
|
|
||||||
not instruction.get("type", "").endswith(SCL_SUFFIX)
|
|
||||||
or "_error" in instruction.get("type", "")
|
|
||||||
or instruction.get("grouped", False)
|
|
||||||
or instr_type_original
|
|
||||||
not in [
|
|
||||||
"Contact",
|
|
||||||
"O",
|
|
||||||
"Eq",
|
|
||||||
"Ne",
|
|
||||||
"Gt",
|
|
||||||
"Lt",
|
|
||||||
"Ge",
|
|
||||||
"Le",
|
|
||||||
"PBox",
|
|
||||||
"NBox",
|
|
||||||
"And",
|
|
||||||
"Xor",
|
|
||||||
"Not",
|
|
||||||
]
|
|
||||||
):
|
|
||||||
return False
|
|
||||||
current_scl = instruction.get("scl", "")
|
|
||||||
if (
|
|
||||||
current_scl.strip().startswith("IF")
|
|
||||||
and "END_IF;" in current_scl
|
|
||||||
and GROUPED_COMMENT not in current_scl
|
|
||||||
):
|
|
||||||
return False
|
|
||||||
map_key_out = (network_id, instr_uid, "out")
|
|
||||||
sympy_condition_expr = sympy_map.get(map_key_out)
|
|
||||||
if sympy_condition_expr is None or sympy_condition_expr in [
|
|
||||||
sympy.true,
|
|
||||||
sympy.false,
|
|
||||||
]:
|
|
||||||
return False
|
|
||||||
|
|
||||||
grouped_instructions_cores = []
|
|
||||||
consumer_instr_list = []
|
|
||||||
network_logic = next(
|
|
||||||
(net["logic"] for net in data["networks"] if net["id"] == network_id), []
|
|
||||||
)
|
|
||||||
if not network_logic:
|
|
||||||
return False
|
|
||||||
groupable_types = [
|
|
||||||
"Move",
|
|
||||||
"Add",
|
|
||||||
"Sub",
|
|
||||||
"Mul",
|
|
||||||
"Div",
|
|
||||||
"Mod",
|
|
||||||
"Convert",
|
|
||||||
"Call_FC",
|
|
||||||
"Call_FB",
|
|
||||||
"SCoil",
|
|
||||||
"RCoil",
|
|
||||||
"BLKMOV",
|
|
||||||
"TON",
|
|
||||||
"TOF",
|
|
||||||
"TP",
|
|
||||||
"Se",
|
|
||||||
"Sd",
|
|
||||||
"CTU",
|
|
||||||
"CTD",
|
|
||||||
"CTUD",
|
|
||||||
]
|
|
||||||
for consumer_instr in network_logic:
|
|
||||||
consumer_uid = consumer_instr["instruction_uid"]
|
|
||||||
if consumer_instr.get("grouped", False) or consumer_uid == instr_uid:
|
|
||||||
continue
|
|
||||||
consumer_en = consumer_instr.get("inputs", {}).get("en")
|
|
||||||
consumer_type = consumer_instr.get("type", "")
|
|
||||||
consumer_type_original = consumer_type.replace(SCL_SUFFIX, "").replace(
|
|
||||||
"_error", ""
|
|
||||||
)
|
|
||||||
is_enabled_by_us = False
|
|
||||||
if (
|
|
||||||
isinstance(consumer_en, dict)
|
|
||||||
and consumer_en.get("type") == "connection"
|
|
||||||
and consumer_en.get("source_instruction_uid") == instr_uid
|
|
||||||
and consumer_en.get("source_pin") == "out"
|
|
||||||
):
|
|
||||||
is_enabled_by_us = True
|
|
||||||
if (
|
|
||||||
is_enabled_by_us
|
|
||||||
and consumer_type.endswith(SCL_SUFFIX)
|
|
||||||
and consumer_type_original in groupable_types
|
|
||||||
):
|
|
||||||
consumer_scl = consumer_instr.get("scl", "")
|
|
||||||
core_scl = None
|
|
||||||
if consumer_scl:
|
|
||||||
if consumer_scl.strip().startswith("IF"):
|
|
||||||
match = re.search(
|
|
||||||
r"IF\s+.*?THEN\s*(.*?)\s*END_IF;",
|
|
||||||
consumer_scl,
|
|
||||||
re.DOTALL | re.IGNORECASE,
|
|
||||||
)
|
|
||||||
core_scl = match.group(1).strip() if match else None
|
|
||||||
elif not consumer_scl.strip().startswith("//"):
|
|
||||||
core_scl = consumer_scl.strip()
|
|
||||||
if core_scl:
|
|
||||||
grouped_instructions_cores.append(core_scl)
|
|
||||||
consumer_instr_list.append(consumer_instr)
|
|
||||||
if len(grouped_instructions_cores) > 1:
|
|
||||||
print(
|
|
||||||
f"INFO: Agrupando {len(grouped_instructions_cores)} instr. bajo condición de {instr_type_original} UID {instr_uid}"
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
simplified_expr = sympy.logic.boolalg.to_dnf(
|
|
||||||
sympy_condition_expr, simplify=True
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error simplifying condition for grouping UID {instr_uid}: {e}")
|
|
||||||
simplified_expr = sympy_condition_expr
|
|
||||||
condition_scl_simplified = sympy_expr_to_scl(simplified_expr, symbol_manager)
|
|
||||||
scl_grouped_lines = [f"IF {condition_scl_simplified} THEN"]
|
|
||||||
for core_line in grouped_instructions_cores:
|
|
||||||
indented_core = "\n".join(
|
|
||||||
[f" {line.strip()}" for line in core_line.splitlines()]
|
|
||||||
)
|
|
||||||
scl_grouped_lines.append(indented_core)
|
|
||||||
scl_grouped_lines.append("END_IF;")
|
|
||||||
final_grouped_scl = "\n".join(scl_grouped_lines)
|
|
||||||
instruction["scl"] = final_grouped_scl
|
|
||||||
for consumer_instr in consumer_instr_list:
|
|
||||||
consumer_instr["scl"] = f"{GROUPED_COMMENT} (by UID {instr_uid})"
|
|
||||||
consumer_instr["grouped"] = True
|
|
||||||
made_change = True
|
|
||||||
return made_change
|
|
||||||
|
|
||||||
|
|
||||||
def load_processors(processors_dir="processors"):
|
|
||||||
processor_map = {}
|
|
||||||
processor_list_unsorted = []
|
|
||||||
default_priority = 10
|
|
||||||
if not os.path.isdir(processors_dir):
|
|
||||||
print(f"Error: Directorio de procesadores no encontrado: '{processors_dir}'")
|
|
||||||
return processor_map, []
|
|
||||||
print(f"Cargando procesadores desde: '{processors_dir}'")
|
|
||||||
processors_package = os.path.basename(processors_dir)
|
|
||||||
for filename in os.listdir(processors_dir):
|
|
||||||
if filename.startswith("process_") and filename.endswith(".py"):
|
|
||||||
module_name_rel = filename[:-3]
|
|
||||||
full_module_name = f"{processors_package}.{module_name_rel}"
|
|
||||||
try:
|
|
||||||
module = importlib.import_module(full_module_name)
|
|
||||||
if hasattr(module, "get_processor_info") and callable(
|
|
||||||
module.get_processor_info
|
|
||||||
):
|
|
||||||
processor_info = module.get_processor_info()
|
|
||||||
info_list = []
|
|
||||||
if isinstance(processor_info, dict):
|
|
||||||
info_list = [processor_info]
|
|
||||||
elif isinstance(processor_info, list):
|
|
||||||
info_list = processor_info
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
f" Advertencia: get_processor_info en {full_module_name} devolvió tipo inesperado."
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
for info in info_list:
|
|
||||||
if (
|
|
||||||
isinstance(info, dict)
|
|
||||||
and "type_name" in info
|
|
||||||
and "processor_func" in info
|
|
||||||
):
|
|
||||||
type_name = info["type_name"].lower()
|
|
||||||
processor_func = info["processor_func"]
|
|
||||||
priority = info.get("priority", default_priority)
|
|
||||||
if callable(processor_func):
|
|
||||||
if type_name in processor_map:
|
|
||||||
print(
|
|
||||||
f" Advertencia: '{type_name}' en {full_module_name} sobrescribe definición anterior."
|
|
||||||
)
|
|
||||||
processor_map[type_name] = processor_func
|
|
||||||
processor_list_unsorted.append(
|
|
||||||
{
|
|
||||||
"priority": priority,
|
|
||||||
"type_name": type_name,
|
|
||||||
"func": processor_func,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
f" Advertencia: 'processor_func' para '{type_name}' en {full_module_name} no es callable."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
f" Advertencia: Entrada inválida en {full_module_name}: {info}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
f" Advertencia: Módulo {module_name_rel}.py no tiene 'get_processor_info'."
|
|
||||||
)
|
|
||||||
except ImportError as e:
|
|
||||||
print(f"Error importando {full_module_name}: {e}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error procesando {full_module_name}: {e}")
|
|
||||||
traceback.print_exc()
|
|
||||||
processor_list_sorted = sorted(processor_list_unsorted, key=lambda x: x["priority"])
|
|
||||||
return processor_map, processor_list_sorted
|
|
||||||
|
|
||||||
|
|
||||||
# --- Bucle Principal de Procesamiento (MODIFICADO para copiar metadatos) ---
|
|
||||||
def process_json_to_scl(json_filepath, output_json_filepath):
|
|
||||||
"""
|
|
||||||
Lee JSON simplificado, copia metadatos, aplica procesadores dinámicos,
|
|
||||||
y guarda JSON procesado en la ruta especificada.
|
|
||||||
"""
|
|
||||||
global data
|
|
||||||
|
|
||||||
if not os.path.exists(json_filepath):
|
|
||||||
print(
|
|
||||||
f"Error Crítico (x2): JSON de entrada no encontrado: {json_filepath}",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
print(f"Cargando JSON desde: {json_filepath}")
|
|
||||||
try:
|
|
||||||
with open(json_filepath, "r", encoding="utf-8") as f:
|
|
||||||
data = json.load(f) # Carga el JSON de entrada
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error Crítico al cargar JSON de entrada: {e}", file=sys.stderr)
|
|
||||||
traceback.print_exc(file=sys.stderr)
|
|
||||||
return False
|
|
||||||
|
|
||||||
# <-- NUEVO: Extraer metadatos del JSON de entrada (si existen) -->
|
|
||||||
source_xml_mod_time = data.get("source_xml_mod_time")
|
|
||||||
source_xml_size = data.get("source_xml_size")
|
|
||||||
# <-- FIN NUEVO -->
|
|
||||||
|
|
||||||
block_type = data.get("block_type", "Unknown")
|
|
||||||
print(f"Procesando bloque tipo: {block_type}")
|
|
||||||
|
|
||||||
if block_type in ["GlobalDB", "PlcUDT", "PlcTagTable", "InstanceDB"]: # <-- MODIFIED: Add InstanceDB
|
|
||||||
print(f"INFO: El bloque es {block_type}. Saltando procesamiento lógico de x2.")
|
|
||||||
print(
|
|
||||||
f"Guardando JSON de {block_type} (con metadatos) en: {output_json_filepath}"
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
# <-- MODIFICADO: Asegurar que los metadatos se guarden aunque se salte -->
|
|
||||||
data_to_save = copy.deepcopy(data) # Copiar datos originales
|
|
||||||
if source_xml_mod_time is not None:
|
|
||||||
data_to_save["source_xml_mod_time"] = source_xml_mod_time
|
|
||||||
if source_xml_size is not None:
|
|
||||||
data_to_save["source_xml_size"] = source_xml_size
|
|
||||||
# <-- FIN MODIFICADO -->
|
|
||||||
with open(output_json_filepath, "w", encoding="utf-8") as f_out:
|
|
||||||
json.dump(data_to_save, f_out, indent=4, ensure_ascii=False)
|
|
||||||
print(f"Guardado de {block_type} completado.")
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error Crítico al guardar JSON de {block_type}: {e}")
|
|
||||||
traceback.print_exc(file=sys.stderr)
|
|
||||||
return False
|
|
||||||
|
|
||||||
print(f"INFO: El bloque es {block_type}. Iniciando procesamiento lógico...")
|
|
||||||
|
|
||||||
script_dir = os.path.dirname(__file__)
|
|
||||||
processors_dir_path = os.path.join(script_dir, "processors")
|
|
||||||
processor_map, sorted_processors = load_processors(processors_dir_path)
|
|
||||||
if not processor_map:
|
|
||||||
print("Error crítico: No se cargaron procesadores. Abortando.", file=sys.stderr)
|
|
||||||
return False
|
|
||||||
|
|
||||||
# (Mapas de acceso y bucle iterativo SIN CAMBIOS relevantes, solo pasan 'data' que ya tiene metadatos)
|
|
||||||
network_access_maps = {}
|
|
||||||
for network in data.get("networks", []):
|
|
||||||
net_id = network["id"]
|
|
||||||
current_access_map = {}
|
|
||||||
for instr in network.get("logic", []):
|
|
||||||
for _, source in instr.get("inputs", {}).items():
|
|
||||||
sources_to_check = (
|
|
||||||
source
|
|
||||||
if isinstance(source, list)
|
|
||||||
else ([source] if isinstance(source, dict) else [])
|
|
||||||
)
|
|
||||||
for src in sources_to_check:
|
|
||||||
if (
|
|
||||||
isinstance(src, dict)
|
|
||||||
and src.get("uid")
|
|
||||||
and src.get("type") in ["variable", "constant"]
|
|
||||||
):
|
|
||||||
current_access_map[src["uid"]] = src
|
|
||||||
for _, dest_list in instr.get("outputs", {}).items():
|
|
||||||
if isinstance(dest_list, list):
|
|
||||||
for dest in dest_list:
|
|
||||||
if (
|
|
||||||
isinstance(dest, dict)
|
|
||||||
and dest.get("uid")
|
|
||||||
and dest.get("type") in ["variable", "constant"]
|
|
||||||
):
|
|
||||||
current_access_map[dest["uid"]] = dest
|
|
||||||
network_access_maps[net_id] = current_access_map
|
|
||||||
|
|
||||||
symbol_manager = SymbolManager()
|
|
||||||
sympy_map = {}
|
|
||||||
max_passes = 30
|
|
||||||
passes = 0
|
|
||||||
processing_complete = False
|
|
||||||
print(f"\n--- Iniciando Bucle de Procesamiento Iterativo ({block_type}) ---")
|
|
||||||
while passes < max_passes and not processing_complete:
|
|
||||||
passes += 1
|
|
||||||
made_change_in_base_pass = False
|
|
||||||
made_change_in_group_pass = False
|
|
||||||
print(f"\n--- Pase {passes} ---")
|
|
||||||
num_sympy_processed_this_pass = 0
|
|
||||||
num_grouped_this_pass = 0
|
|
||||||
print(f" Fase 1 (SymPy Base - Orden por Prioridad):")
|
|
||||||
num_sympy_processed_this_pass = 0
|
|
||||||
for processor_info in sorted_processors:
|
|
||||||
current_type_name = processor_info["type_name"]
|
|
||||||
func_to_call = processor_info["func"]
|
|
||||||
for network in data.get("networks", []):
|
|
||||||
network_id = network["id"]
|
|
||||||
network_lang = network.get("language", "LAD")
|
|
||||||
if network_lang == "STL":
|
|
||||||
continue
|
|
||||||
access_map = network_access_maps.get(network_id, {})
|
|
||||||
network_logic = network.get("logic", [])
|
|
||||||
for instruction in network_logic:
|
|
||||||
instr_uid = instruction.get("instruction_uid")
|
|
||||||
instr_type_current = instruction.get("type", "Unknown")
|
|
||||||
if (
|
|
||||||
instr_type_current.endswith(SCL_SUFFIX)
|
|
||||||
or "_error" in instr_type_current
|
|
||||||
or instruction.get("grouped", False)
|
|
||||||
or instr_type_current
|
|
||||||
in [
|
|
||||||
"RAW_STL_CHUNK",
|
|
||||||
"RAW_SCL_CHUNK",
|
|
||||||
"UNSUPPORTED_LANG",
|
|
||||||
"UNSUPPORTED_CONTENT",
|
|
||||||
"PARSING_ERROR",
|
|
||||||
]
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
lookup_key = instr_type_current.lower()
|
|
||||||
effective_type_name = lookup_key
|
|
||||||
if instr_type_current == "Call":
|
|
||||||
call_block_type = instruction.get("block_type", "").upper()
|
|
||||||
if call_block_type == "FC":
|
|
||||||
effective_type_name = "call_fc"
|
|
||||||
elif call_block_type == "FB":
|
|
||||||
effective_type_name = "call_fb"
|
|
||||||
if effective_type_name == current_type_name:
|
|
||||||
try:
|
|
||||||
changed = func_to_call(
|
|
||||||
instruction, network_id, sympy_map, symbol_manager, data
|
|
||||||
) # data se pasa aquí
|
|
||||||
if changed:
|
|
||||||
made_change_in_base_pass = True
|
|
||||||
num_sympy_processed_this_pass += 1
|
|
||||||
except Exception as e:
|
|
||||||
print(
|
|
||||||
f"ERROR(SymPy Base) al procesar {instr_type_current} UID {instr_uid}: {e}"
|
|
||||||
)
|
|
||||||
traceback.print_exc()
|
|
||||||
instruction["scl"] = (
|
|
||||||
f"// ERROR en SymPy procesador base: {e}"
|
|
||||||
)
|
|
||||||
instruction["type"] = instr_type_current + "_error"
|
|
||||||
made_change_in_base_pass = True
|
|
||||||
print(
|
|
||||||
f" -> {num_sympy_processed_this_pass} instrucciones (no STL) procesadas con SymPy."
|
|
||||||
)
|
|
||||||
|
|
||||||
if made_change_in_base_pass or passes == 1:
|
|
||||||
print(f" Fase 2 (Agrupación IF con Simplificación):")
|
|
||||||
num_grouped_this_pass = 0
|
|
||||||
for network in data.get("networks", []):
|
|
||||||
network_id = network["id"]
|
|
||||||
network_lang = network.get("language", "LAD")
|
|
||||||
if network_lang == "STL":
|
|
||||||
continue
|
|
||||||
network_logic = network.get("logic", [])
|
|
||||||
uids_in_network = sorted(
|
|
||||||
[
|
|
||||||
instr.get("instruction_uid", "Z")
|
|
||||||
for instr in network_logic
|
|
||||||
if instr.get("instruction_uid")
|
|
||||||
]
|
|
||||||
)
|
|
||||||
for uid_to_process in uids_in_network:
|
|
||||||
instruction = next(
|
|
||||||
(
|
|
||||||
instr
|
|
||||||
for instr in network_logic
|
|
||||||
if instr.get("instruction_uid") == uid_to_process
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
if not instruction:
|
|
||||||
continue
|
|
||||||
if instruction.get("grouped") or "_error" in instruction.get(
|
|
||||||
"type", ""
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
if instruction.get("type", "").endswith(SCL_SUFFIX):
|
|
||||||
try:
|
|
||||||
group_changed = process_group_ifs(
|
|
||||||
instruction, network_id, sympy_map, symbol_manager, data
|
|
||||||
) # data se pasa aquí
|
|
||||||
if group_changed:
|
|
||||||
made_change_in_group_pass = True
|
|
||||||
num_grouped_this_pass += 1
|
|
||||||
except Exception as e:
|
|
||||||
print(
|
|
||||||
f"ERROR(GroupLoop) al intentar agrupar desde UID {instruction.get('instruction_uid')}: {e}"
|
|
||||||
)
|
|
||||||
traceback.print_exc()
|
|
||||||
print(
|
|
||||||
f" -> {num_grouped_this_pass} agrupaciones realizadas (en redes no STL)."
|
|
||||||
)
|
|
||||||
|
|
||||||
if not made_change_in_base_pass and not made_change_in_group_pass:
|
|
||||||
print(
|
|
||||||
f"\n--- No se hicieron más cambios en el pase {passes}. Proceso iterativo completado. ---"
|
|
||||||
)
|
|
||||||
processing_complete = True
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
f"--- Fin Pase {passes}: {num_sympy_processed_this_pass} proc SymPy, {num_grouped_this_pass} agrup. Continuando..."
|
|
||||||
)
|
|
||||||
if passes == max_passes and not processing_complete:
|
|
||||||
print(f"\n--- ADVERTENCIA: Límite de {max_passes} pases alcanzado...")
|
|
||||||
|
|
||||||
# --- Verificación Final y Guardado JSON ---
|
|
||||||
print(f"\n--- Verificación Final de Instrucciones No Procesadas ({block_type}) ---")
|
|
||||||
unprocessed_count = 0
|
|
||||||
unprocessed_details = []
|
|
||||||
ignored_types = [
|
|
||||||
"raw_scl_chunk",
|
|
||||||
"unsupported_lang",
|
|
||||||
"raw_stl_chunk",
|
|
||||||
"unsupported_content",
|
|
||||||
"parsing_error",
|
|
||||||
]
|
|
||||||
for network in data.get("networks", []):
|
|
||||||
network_id = network.get("id", "Unknown ID")
|
|
||||||
network_title = network.get("title", f"Network {network_id}")
|
|
||||||
network_lang = network.get("language", "LAD")
|
|
||||||
if network_lang == "STL":
|
|
||||||
continue
|
|
||||||
for instruction in network.get("logic", []):
|
|
||||||
instr_uid = instruction.get("instruction_uid", "Unknown UID")
|
|
||||||
instr_type = instruction.get("type", "Unknown Type")
|
|
||||||
is_grouped = instruction.get("grouped", False)
|
|
||||||
if (
|
|
||||||
not instr_type.endswith(SCL_SUFFIX)
|
|
||||||
and "_error" not in instr_type
|
|
||||||
and not is_grouped
|
|
||||||
and instr_type.lower() not in ignored_types
|
|
||||||
):
|
|
||||||
unprocessed_count += 1
|
|
||||||
unprocessed_details.append(
|
|
||||||
f" - Red '{network_title}' (ID: {network_id}, Lang: {network_lang}), Instrucción UID: {instr_uid}, Tipo: '{instr_type}'"
|
|
||||||
)
|
|
||||||
if unprocessed_count > 0:
|
|
||||||
print(
|
|
||||||
f"ADVERTENCIA: Se encontraron {unprocessed_count} instrucciones (no STL) que parecen no haber sido procesadas:"
|
|
||||||
)
|
|
||||||
[print(detail) for detail in unprocessed_details]
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
"INFO: Todas las instrucciones relevantes (no STL) parecen haber sido procesadas o agrupadas."
|
|
||||||
)
|
|
||||||
|
|
||||||
# <-- MODIFICADO: Asegurar que los metadatos se añadan al 'data' final antes de guardar -->
|
|
||||||
if source_xml_mod_time is not None:
|
|
||||||
data["source_xml_mod_time"] = source_xml_mod_time
|
|
||||||
if source_xml_size is not None:
|
|
||||||
data["source_xml_size"] = source_xml_size
|
|
||||||
# <-- FIN MODIFICADO -->
|
|
||||||
|
|
||||||
print(f"\nGuardando JSON procesado ({block_type}) en: {output_json_filepath}")
|
|
||||||
try:
|
|
||||||
with open(output_json_filepath, "w", encoding="utf-8") as f:
|
|
||||||
json.dump(
|
|
||||||
data, f, indent=4, ensure_ascii=False
|
|
||||||
) # Guardar el diccionario 'data' modificado
|
|
||||||
print("Guardado completado.")
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error Crítico al guardar JSON procesado: {e}", file=sys.stderr)
|
|
||||||
traceback.print_exc(file=sys.stderr)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
# --- Ejecución (MODIFICADO) ---
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# Lógica para ejecución standalone
|
|
||||||
try:
|
|
||||||
import tkinter as tk
|
|
||||||
from tkinter import filedialog
|
|
||||||
except ImportError:
|
|
||||||
print("Error: Tkinter no está instalado. No se puede mostrar el diálogo de archivo.", file=sys.stderr)
|
|
||||||
tk = None
|
|
||||||
|
|
||||||
input_json_file = ""
|
|
||||||
if tk:
|
|
||||||
root = tk.Tk()
|
|
||||||
root.withdraw()
|
|
||||||
print("Por favor, selecciona el archivo JSON de entrada (generado por x1)...")
|
|
||||||
input_json_file = filedialog.askopenfilename(
|
|
||||||
title="Selecciona el archivo JSON de entrada (.json)",
|
|
||||||
filetypes=[("JSON files", "*.json"), ("All files", "*.*")]
|
|
||||||
)
|
|
||||||
root.destroy()
|
|
||||||
|
|
||||||
if not input_json_file:
|
|
||||||
print("No se seleccionó ningún archivo. Saliendo.", file=sys.stderr)
|
|
||||||
else:
|
|
||||||
print(f"Archivo JSON de entrada seleccionado: {input_json_file}")
|
|
||||||
|
|
||||||
# Calcular ruta de salida JSON procesado
|
|
||||||
json_filename_base = os.path.splitext(os.path.basename(input_json_file))[0]
|
|
||||||
# Asumimos que el _processed.json va al mismo directorio 'parsing'
|
|
||||||
parsing_dir = os.path.dirname(input_json_file)
|
|
||||||
output_json_file = os.path.join(parsing_dir, f"{json_filename_base}_processed.json")
|
|
||||||
|
|
||||||
# Asegurarse de que el directorio de salida exista (aunque debería si el input existe)
|
|
||||||
os.makedirs(parsing_dir, exist_ok=True)
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"(x2 - Standalone) Procesando: '{os.path.relpath(input_json_file)}' -> '{os.path.relpath(output_json_file)}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
success = process_json_to_scl(input_json_file, output_json_file)
|
|
||||||
if success:
|
|
||||||
print("\nProcesamiento completado exitosamente.")
|
|
||||||
else:
|
|
||||||
print(f"\nError durante el procesamiento de '{os.path.relpath(input_json_file)}'.", file=sys.stderr)
|
|
||||||
# sys.exit(1) # No usar sys.exit
|
|
||||||
except Exception as e:
|
|
||||||
print(
|
|
||||||
f"Error Crítico (x2) durante el procesamiento de '{input_json_file}': {e}",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
traceback.print_exc(file=sys.stderr)
|
|
||||||
# sys.exit(1) # No usar sys.exit
|
|
File diff suppressed because it is too large
Load Diff
|
@ -15,5 +15,5 @@
|
||||||
"xref_source_subdir": "source"
|
"xref_source_subdir": "source"
|
||||||
},
|
},
|
||||||
"level3": {},
|
"level3": {},
|
||||||
"working_directory": "D:\\Trabajo\\VM\\22 - 93841 - Sidel - Tilting\\Reporte\\TiaExports"
|
"working_directory": "C:\\Trabajo\\SIDEL\\13 - E5.007560 - Modifica O&U - SAE235\\Reporte\\ExportTia"
|
||||||
}
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"path": "D:\\Trabajo\\VM\\22 - 93841 - Sidel - Tilting\\Reporte\\TiaExports",
|
"path": "C:\\Trabajo\\SIDEL\\13 - E5.007560 - Modifica O&U - SAE235\\Reporte\\ExportTia",
|
||||||
"history": [
|
"history": [
|
||||||
|
"C:\\Trabajo\\SIDEL\\13 - E5.007560 - Modifica O&U - SAE235\\Reporte\\ExportTia",
|
||||||
"D:\\Trabajo\\VM\\22 - 93841 - Sidel - Tilting\\Reporte\\TiaExports",
|
"D:\\Trabajo\\VM\\22 - 93841 - Sidel - Tilting\\Reporte\\TiaExports",
|
||||||
"D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source",
|
"D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source",
|
||||||
"C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\SourceDoc\\SourceXML",
|
"C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\SourceDoc\\SourceXML",
|
||||||
|
|
18545
data/log.txt
18545
data/log.txt
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue