# LAD-to-SCL Conversion Pipeline: Documentación de Referencia ## 1. Visión General Este documento describe un pipeline de scripts de Python diseñado para convertir bloques de función o funciones (FC/FB) escritos en Ladder Logic (LAD) desde archivos XML de TIA Portal Openness a un código SCL (Structured Control Language) semánticamente equivalente. El proceso se divide en tres etapas principales, cada una manejada por un script específico: 1. **XML a JSON Enriquecido (`x1_to_json.py`):** Parsea el XML de Openness, extrae la estructura lógica (incluyendo llamadas a FC/FB y temporizadores S5), conexiones explícitas e **infiere conexiones implícitas** (especialmente las habilitaciones EN) para crear un archivo JSON detallado. Mapea tipos de instrucción LAD/FBD (p.ej., `Se`, `NBox`) a nombres internos consistentes (p.ej., `TON_S5`, `N_TRIG`). 2. **Procesamiento Semántico (`x2_process.py`):** Lee el JSON enriquecido y, de forma iterativa, traduce cada instrucción (usando los tipos mapeados) a su equivalente SCL, manejando dependencias, propagando el estado lógico (RLO), traduciendo temporizadores S5 a IEC, generando lógica de flancos y agrupando lógica paralela bajo bloques `IF`. El SCL generado se almacena *dentro* del propio JSON. 3. **Generación de SCL Final (`x3_generate_scl.py`):** Lee el JSON completamente procesado y ensambla el código SCL final en un archivo `.scl` formateado, incluyendo declaraciones de variables (Input, Output, InOut, Temp, Stat - incluyendo instancias de temporizadores y bits de memoria de flancos) y el cuerpo del programa (`FUNCTION` o `FUNCTION_BLOCK`). ## 2. Etapas del Pipeline ### Etapa 1: XML a JSON Enriquecido (`x1_to_json.py`) * **Propósito:** Transformar la compleja y a veces ambigua estructura XML de Openness en un formato JSON estructurado y más fácil de procesar, añadiendo información clave que está implícita en el LAD visual pero no siempre explícita en el XML. * **Entrada:** Archivo `.xml` exportado desde TIA Portal Openness para un FC o FB. * **Salida:** Archivo `_simplified.json`. * **Proceso Clave:** 1. **Parseo XML:** Utiliza `lxml` para leer el archivo XML. 2. **Extracción de Metadatos:** Obtiene nombre del bloque, número, lenguaje original, comentario del bloque. Detecta si es `SW.Blocks.FC` o `SW.Blocks.FB`. 3. **Extracción de Interfaz:** Parsea las secciones `Input`, `Output`, `InOut`, `Temp`, `Constant`, `Return` para obtener la declaración de variables. 4. **Parseo de Redes (`CompileUnit`):** Itera sobre cada red lógica. * **`parse_network`:** * **Parseo de Componentes:** * `Access`: Identifica variables globales/locales, constantes literales y tipadas (`parse_access`). Utiliza `get_symbol_name` para formatear nombres simbólicos con comillas (`"DB"."Var"`). * `Part`: Identifica instrucciones estándar LAD/FBD (`parse_part`). **Importante:** Detecta pines negados (``) y los almacena en `negated_pins`. **Mapea** nombres XML (`Se`, `Sd`, `PBox`, `NBox`, `RCoil`, `SCoil`, `SdCoil`) a tipos internos (`TON_S5`, `TONR_S5`, `P_TRIG`, `N_TRIG`, `R`, `S`, `SR`). * `Call`: Identifica llamadas a otros FCs o FBs (`parse_call`), extrayendo el nombre del bloque llamado, tipo (FC/FB) e información de instancia DB (`instance_db`) si aplica (formateado con comillas). * Crea `access_map` y `parts_and_calls_map` para referencia rápida por UID. * **Parseo de Conexiones (`Wire`):** * Construye `wire_connections`: Un mapa `(dest_uid, dest_pin) -> [(src_uid, src_pin), ...]`. * Construye `source_connections`: Un mapa `(src_uid, src_pin) -> [(dest_uid, dest_pin), ...]`. * **Construcción Lógica Inicial:** Crea una lista de diccionarios (`all_logic_steps`), uno por cada `Part` o `Call`, rellenando `inputs` y `outputs` **solo con las conexiones explícitas** encontradas en los `Wire`. * **Inferencia de Conexión `EN` (¡Paso Crucial!):** * Itera sobre los bloques funcionales (`Move`, `Add`, `Call`, `BLKMOV`, etc.) que *no* tienen una entrada `en` explícita definida después del paso anterior. * Utiliza una **heurística de búsqueda lineal hacia atrás** para encontrar la fuente de RLO más probable que precede a este bloque (la salida `out` de un bloque lógico como `Contact`/`O`/`Eq`/`P_TRIG`/`N_TRIG` o la salida `eno` de un bloque funcional anterior). * Si se encuentra una fuente inferida, **añade la conexión `en`** al diccionario `inputs` del bloque funcional actual. Esto enriquece el JSON con la dependencia lógica implícita. * **Ordenamiento:** Ordena las instrucciones en la lista `logic` final (generalmente por UID). 5. **Escritura JSON:** Guarda la estructura de datos completa en el archivo `_simplified.json`. ### Etapa 2: Procesamiento Semántico (`x2_process.py`) * **Propósito:** Traducir la lógica LAD/FBD (representada en el JSON enriquecido) a código SCL embebido, resolviendo dependencias entre instrucciones, generando lógica SCL equivalente (incluyendo manejo de flancos y temporizadores), y aplicando optimizaciones de agrupación `IF`. * **Entrada:** Archivo `_simplified.json` (generado por la Etapa 1). * **Salida:** Archivo `_simplified_processed.json`. * **Proceso Clave:** 1. **Carga de JSON y Preparación:** Lee el JSON de entrada y reconstruye mapas de acceso por red. Inicializa el `scl_map` global (o por red). 2. **Bucle Iterativo:** Repite los siguientes pasos hasta que no se realicen cambios en un pase completo o se alcance un límite máximo de pases (`max_passes`). * **Fase 1: Procesadores Base:** * Itera sobre cada instrucción en cada red. * Si la instrucción no ha sido procesada (`_scl` o `_error`) ni agrupada (`grouped`), busca el procesador adecuado (`process_xxx`) basado en su `type` (usando los tipos mapeados como `TON_S5`, `P_TRIG`, etc.). * **`process_xxx` (Ejecución):** * Llama a `get_scl_representation` para obtener el SCL de sus pines de entrada, buscándolos en `scl_map` o directamente en `access_map`. `get_scl_representation` ahora **convierte constantes S5T# a T#**. * Si alguna dependencia no está resuelta (devuelve `None`), el procesador retorna `False`. * Si las dependencias están resueltas: * **Generadores RLO (`Contact`, `O`, `Eq`, `P_TRIG`, `N_TRIG`):** Calculan la expresión booleana SCL resultante y la almacenan en `scl_map` bajo la clave `(network_id, instr_uid, 'out')`. `P_TRIG`/`N_TRIG` generan lógica explícita de flancos y la llamada para actualizar el bit de memoria (`stat_... := CLK;`). Guardan comentarios/lógica en `instruction['scl']`. * **Bloques Funcionales (`Move`, `Add`, `Convert`, `Mod`, `BLKMOV`, `Call`):** * Obtienen la condición `EN` (explícita o inferida). * Generan el código SCL *core* (la asignación, cálculo o llamada). `process_call` ahora usa correctamente `instance_db` para FBs. `process_blkmov` traduce a `DST := SRC;` (con advertencia sobre tipos). * Generan el SCL final, **envolviendo el core en `IF en_scl THEN ... END_IF;`** si `en_scl` no es `"TRUE"`. * Almacenan el SCL final en `instruction['scl']`. * Almacenan el nombre de la variable de salida/temporal (prefijado con `#` si es temporal) en `scl_map` para `out`/`out1`/`RET_VAL`. * Almacenan el `en_scl` en `scl_map` para `eno`. * **Bobinas (`Coil`, `R`, `S`, `SR`):** Obtienen el RLO de entrada (`in`, o `S`/`R1` para `SR`), obtienen el operando destino, generan la asignación (`:= TRUE`/`:= FALSE`) o lógica `IF/ELSIF` envuelta en `IF RLO THEN ...` si aplica, y la guardan en `instruction['scl']`. * **Temporizadores (`TON_S5`, `TONR_S5`):** Generan una llamada a un bloque IEC (`TON`/`TONR`) usando un nombre de instancia (`stat_timer_...`). Parsean el valor `PT` (T#...). Mapean `Q` y `ET` a la instancia (`Instance.Q`, `Instance.ET`) en `scl_map`. Almacenan la llamada en `instruction['scl']`. * Marcan la instrucción como procesada añadiendo `_scl` a `instruction['type']`. * Retornan `True`. * Se registra si hubo algún cambio en esta fase (`made_change_in_base_pass`). * **Fase 2: Agrupación de IFs (`process_group_ifs`):** * Itera sobre las instrucciones *ya procesadas* (`_scl`) que son generadoras de condición (`Contact`, `O`, `Eq`, `P_TRIG`, `N_TRIG`, etc.). * Obtiene la `condition_scl` de `scl_map`. * Busca todos los bloques funcionales, bobinas o temporizadores (`Move_scl`, `Add_scl`, `Coil_scl`, `TON_S5_scl`, etc.) cuyo pin de habilitación (`en`, `in`, `s`) esté conectado a la salida `out` de esta instrucción generadora. * Si encuentra **más de uno**: * Extrae el código *core* (lo que está dentro del `IF...END_IF;` si existe, o el código completo si no) de cada consumidor. * Construye un **único bloque `IF condition_scl THEN ... END_IF;`** que contiene todos los cores extraídos, indentados. * **Sobrescribe** el campo `scl` de la instrucción *generadora de condición* con este nuevo bloque `IF` agrupado. * Marca cada instrucción consumidora con `grouped = True` y cambia su `scl` a un comentario (`GROUPED_COMMENT`) para evitar que `x3_generate_scl.py` lo use. * Se registra si hubo algún cambio en esta fase (`made_change_in_group_pass`). * **Condición de Salida:** El bucle termina si `made_change_in_base_pass` y `made_change_in_group_pass` son ambos `False`. 3. **Verificación Final:** Comprueba si quedaron instrucciones sin procesar, sin agrupar y sin errores, e informa al usuario. 4. **Escritura JSON:** Guarda el JSON modificado (con SCL embebido y marcas de agrupación) en el archivo `_simplified_processed.json`. ### Etapa 3: Generación de SCL Final (`x3_generate_scl.py`) * **Propósito:** Ensamblar un archivo `.scl` completo y formateado a partir del JSON procesado. * **Entrada:** Archivo `_simplified_processed.json` (generado por la Etapa 2). * **Salida:** Archivo `.scl`. * **Proceso Clave:** 1. **Carga de JSON:** Lee el archivo JSON procesado. 2. **Detección de Variables y Tipo de Bloque:** * Escanea el SCL generado en busca de variables `#_temp_...` y `stat_...`. * Identifica los tipos de las variables `stat_...` (Bool, TON, TONR) según sus nombres (`stat_nbox_mem...`, `stat_timer_Se...`, etc.). * Determina si el bloque debe ser `FUNCTION_BLOCK` (si hay variables `STAT` o `TEMP`) o `FUNCTION`. 3. **Generación de Cabecera:** Escribe el encabezado del bloque (`FUNCTION_BLOCK name` o `FUNCTION name`, `VERSION`, etc.). Utiliza `format_variable_name` (la versión corregida) para el nombre del bloque. 4. **Generación de Declaraciones VAR:** * Escribe las secciones `VAR_INPUT`, `VAR_OUTPUT`, `VAR_IN_OUT` usando `format_variable_name` para los nombres de las variables de la interfaz. * **Escribe `VAR_STAT`:** Declara las variables `stat_...` detectadas con sus tipos inferidos (Bool, TON, TONR) y nombres entre comillas. * **Escribe `VAR_TEMP`:** Declara las variables `#_temp_...` detectadas (declaradas como `"_temp_..."` sin el `#` pero con comillas) y las variables de la sección `Temp` de la interfaz. Utiliza inferencia de tipo básica para las variables `#_temp_...`. 5. **Generación del Cuerpo (`BEGIN`/`END_FUNCTION_BLOCK` o `END_FUNCTION`):** * Itera sobre las `networks` en el JSON. * Añade comentarios de red. * Itera sobre la `logic` de cada red. * Para cada `instruction`: * **Verifica el flag `grouped`:** Si `instruction.get('grouped', False)` es `True`, **ignora** esta instrucción. * Si no está agrupada, obtiene el valor del campo `scl`. * **Limpia comentarios internos:** Elimina comentarios informativos específicos (`// RLO:`, `// Comparison:`, `// Logic O:`, `// N/P_TRIG Output Logic:`) si son la única parte de la línea, pero conserva los comentarios de actualización de memoria de flancos, errores y agrupación. * Indenta y añade las líneas del SCL resultante al output. * Añade líneas en blanco entre redes si contienen código. 6. **Escritura de Archivo:** Escribe el string SCL completo al archivo `.scl`. ## 3. Cómo Extender para Nuevas Instrucciones LAD/FBD Añadir soporte para un nuevo tipo de instrucción (p.ej., un comparador `GT`, una función matemática `SQRT`, o un temporizador IEC `TP`) requiere modificar los scripts `x1_to_json.py` y `x2_process.py`. **Pasos:** 1. **Analizar el XML:** Exporta un bloque simple que use la nueva instrucción y examina el XML de Openness. Identifica: * Si se representa como `` o ``. * Sus pines de entrada y salida (`NameCon`/`IdentCon` en los `Wire`). * Cualquier atributo o `TemplateValue` relevante. * Si tiene pines negados (``). 2. **Modificar `x1_to_json.py` (`parse_part` o `parse_call` y `parse_network`):** * **Parseo:** Asegúrate de que `parse_part` o `parse_call` capture correctamente la nueva instrucción. * **Mapeo de Tipo:** Si el nombre XML no es ideal (como `Se`), mapéalo a un nombre interno consistente en `parse_part` (p.ej., si fuera un temporizador IEC TP, podrías mapear `TPartName` -> `TP_IEC`). * **Clasificación:** Decide si la nueva instrucción es un `functional_block_type` (necesita inferencia EN?) o un `rlo_generator`. Actualiza estas listas en `parse_network` si es necesario. * **Inferencia EN:** Revisa si la lógica de inferencia EN en `parse_network` necesita ajustes. * **Pines:** Asegúrate de que sus pines de entrada/salida esperados estén en `possible_input_pins` / `possible_output_pins` en `parse_network`. 3. **Modificar `x2_process.py` (continuación):** * **Lógica SCL (continuación):** * ... * Almacena el SCL final en `instruction['scl']`. * Actualiza `instruction['type']` con el sufijo `_scl`. * **Actualiza `scl_map`:** Añade entradas para los pines de salida (`out`, `Q`, etc.) con su representación SCL (puede ser un nombre de variable temporal prefijado con `#`, el resultado de una expresión, o el acceso a un miembro de instancia como `"Instance".Q`). Añade la entrada para `eno` si el bloque lo tiene (generalmente `scl_map[key_eno] = en_scl`). * Retorna `True`. * **Añadir a la Lista:** Inserta la nueva función `process_nombre_instruccion` en la lista `base_processors_list` en el orden de prioridad correcto (generalmente, generadores de valores/condiciones antes que consumidores, y las llamadas (`process_call`) al final o cerca del final). Actualiza el `processor_map` si es necesario (especialmente si manejas tipos como `Call_FC`, `Call_FB`). * **Agrupación:** Considera si este nuevo bloque debería ser parte de la lógica de agrupación (¿es un bloque funcional, bobina o temporizador que puede ser habilitado en paralelo?). Si es así, añádelo a la lista `groupable_types_original` en `process_group_ifs`. El código de agrupación existente debería funcionar si el procesador del nuevo bloque genera correctamente la estructura `IF EN THEN ... END_IF;` (o código simple si EN=TRUE). 4. **Modificar `x3_generate_scl.py`:** * **Declaraciones STAT:** Si la nueva instrucción introduce la necesidad de un nuevo tipo de variable estática (p.ej., un tipo de datos específico para un contador, o un nuevo tipo de instancia de FB IEC), necesitarás: * Añadir una nueva expresión regular (`stat_pattern_xxx`) para detectar el nombre de la instancia/variable estática generada por tu nuevo procesador en `x2_process.py`. * Actualizar el bucle de detección para que use esta nueva regex y almacene el nombre y el tipo SCL correcto (`MyCounterType`, `IEC_Counter`, etc.) en el diccionario `stat_vars`. * La lógica existente en `generate_scl` que escribe la sección `VAR_STAT` usará esta información para declarar la variable correctamente. * **Declaraciones TEMP:** Si la nueva instrucción genera variables temporales con un patrón específico o requiere un tipo de dato específico que pueda ser inferido, puedes mejorar la lógica de inferencia de tipo en la sección `VAR_TEMP`. * **Limpieza de Comentarios:** Si el nuevo procesador genera comentarios internos específicos que no quieres en el SCL final, ajusta la lógica de filtrado en la sección de generación del cuerpo del bloque. 5. **Probar:** * Crea un archivo XML de prueba simple que use la nueva instrucción en diferentes contextos (con entradas/salidas conectadas, con EN explícito/implícito, negaciones si aplica). * Ejecuta el pipeline completo (`x1_to_json.py`, `x2_process.py`, `x3_generate_scl.py`). * **Verifica `_simplified.json`:** Asegúrate de que `x1` parseó correctamente la instrucción, mapeó su tipo, identificó sus conexiones y realizó la inferencia EN si era necesario. * **Verifica `_simplified_processed.json`:** Comprueba que `x2` ejecutó tu nuevo procesador, generó el SCL esperado en el campo `scl`, marcó el tipo con `_scl`, y actualizó el `scl_map` correctamente para sus salidas. Verifica si la agrupación `IF` funcionó como se esperaba si la instrucción era parte de lógica paralela. * **Verifica el archivo `.scl`:** Confirma que `x3` generó el SCL final correctamente, incluyendo las declaraciones necesarias (STAT/TEMP) y el código SCL de la instrucción (indentado y sin comentarios no deseados). Asegúrate de que no haya errores de sintaxis SCL obvios. * **Importar en TIA Portal (Opcional pero Recomendado):** Intenta importar el archivo SCL generado en un proyecto de TIA Portal para validar la sintaxis y la estructura del bloque. ## 4. Futuras Mejoras y Consideraciones * **Manejo de Tipos de Datos:** Implementar un seguimiento y conversión de tipos más robusto. Inferir tipos para variables temporales de forma más precisa. Usar funciones de conversión SCL explícitas (`INT_TO_DINT`, etc.) donde sea necesario, posiblemente requiriendo información de tipo adicional en el JSON. * **Lógica de Flancos/Temporizadores:** Asegurar que la traducción de temporizadores S5 y la lógica de flancos generada sea completamente compatible con los bloques IEC estándar (`TON`, `TOF`, `TP`, `TONR`, `R_TRIG`, `F_TRIG`). Considerar la necesidad de declarar instancias de `R_TRIG`/`F_TRIG` en `VAR_STAT` en lugar de generar lógica explícita. * **Soporte para FBs:** Mejorar el manejo de parámetros InOut y Return para llamadas a FC/FB. Potencialmente, requerir información de la interfaz del bloque llamado (parseando su propio XML o desde una biblioteca) para generar llamadas SCL más precisas. * **Optimización SCL:** Explorar más optimizaciones SCL más allá de la agrupación de IFs (p.ej., simplificación de expresiones booleanas complejas, propagación de constantes). * **Estructuras LAD Complejas:** La inferencia de EN actual (búsqueda lineal hacia atrás) es simple. Para manejar bifurcaciones (`Branch`), uniones (`Merge`) y saltos (`JMP`) complejos de forma robusta, `x1_to_json.py` necesitaría realizar un análisis de flujo de datos/control más sofisticado para determinar las dependencias lógicas correctas antes de generar el JSON. * **Manejo de Errores:** Mejorar el reporte de errores con más contexto (nombre de red, UID, tipo de instrucción). Añadir más validaciones en cada etapa. * **Interfaz Gráfica/Configuración:** Crear una interfaz más amigable o archivos de configuración para gestionar el proceso, seleccionar archivos y configurar opciones (p.ej., idioma por defecto, nivel de log). * **Soporte para otros lenguajes (FBD/STL):** Extender el parser `x1_to_json.py` y los procesadores en `x2_process.py` para manejar otros lenguajes de origen, lo cual requeriría entender sus respectivas representaciones XML en Openness. ## 5. Conclusión Este pipeline proporciona una base funcional para la conversión automática de LAD a SCL desde archivos TIA Portal Openness XML. La clave de su funcionamiento es la **separación de responsabilidades**: `x1` enriquece el modelo de datos con información implícita, `x2` realiza la traducción semántica iterativa y la optimización de agrupación, y `x3` ensambla el archivo SCL final. Al añadir procesadores específicos para cada tipo de instrucción LAD/FBD y refinar la lógica de inferencia y generación, la cobertura y calidad de la conversión pueden ser extendidas significativamente. La estructura modular facilita la incorporación de soporte para nuevas instrucciones y futuras mejoras.