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:
Miguel 2025-06-19 18:05:47 +02:00
parent 5da7dcad06
commit f57d0f21dc
13 changed files with 31565 additions and 9331 deletions

293
.doc/backend_setup.md Normal file
View File

@ -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

View File

@ -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"
} }

View File

@ -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",

View File

@ -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.

View File

@ -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
}
}
```

View File

@ -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,13 +128,27 @@ 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"""
network = { network = {
@ -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")
final_expr = simplified
else:
# 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}")
# Verificar si el post-procesamiento cambió algo # Post-procesar para eliminar contradicciones
if str(final_expr) != str(dnf_expr): final_expr = self._post_process_expression(dnf_expr)
print(f" Post-procesada: {final_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", {})
converter.parse_file(file_path) # 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
print(f"Redes encontradas: {len(converter.networks)}") # Verificar directorio de trabajo
print(f"Secciones de variables: {list(converter.var_sections.keys())}") if not os.path.exists(working_directory):
print(f"ACTIONs encontradas: {list(converter.actions.keys())}") print(f"Error: El directorio de trabajo no existe: {working_directory}")
return
# Mostrar información de debug # Crear directorio de salida SCL
converter.print_debug_info() full_scl_path = os.path.join(working_directory, scl_output_dir)
if not os.path.exists(full_scl_path):
os.makedirs(full_scl_path)
print(f"Directorio creado: {full_scl_path}")
# NUEVO: Optimizar expresiones con SymPy # Buscar todos los archivos .EXP
converter.optimize_expressions() exp_pattern = os.path.join(working_directory, "*.EXP")
exp_files = glob.glob(exp_pattern)
# NUEVO: Analizar agrupación de condiciones if not exp_files:
converter.group_common_conditions() print(f"No se encontraron archivos .EXP en: {working_directory}")
return
# Convertir y guardar print(f"Encontrados {len(exp_files)} archivos .EXP en: {working_directory}")
print("\nGenerando código SCL completo...") print(f"Directorio de salida SCL: {full_scl_path}")
structured_code = converter.save_to_file(f"{output_name}.txt") print()
# Mostrar el código generado # Procesar cada archivo
lines = structured_code.split('\n') successful_conversions = 0
print(f"\nCódigo SCL generado ({len(lines)} líneas):") failed_conversions = 0
for i, line in enumerate(lines):
print(f"{i+1:3d}: {line}")
print(f"\n✓ Conversión SCL completada!") for exp_file in exp_files:
filename = os.path.basename(exp_file)
base_name = os.path.splitext(filename)[0]
scl_filename = f"{base_name}.scl"
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()
# 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()

View File

@ -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

View File

@ -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"
} }

View File

@ -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

File diff suppressed because one or more lines are too long