# Parser de Archivos de Definición de DB y UDT de Step 7 (Formato Texto) ## 1. Introducción Este documento describe el proceso y la arquitectura de un parser diseñado para analizar archivos de texto que contienen definiciones de Bloques de Datos (DB) y Tipos de Datos de Usuario (UDT) exportados desde Siemens Step 7 (o TIA Portal en formato AWL/STL). El objetivo principal del parser es extraer la estructura de estos bloques, incluyendo los nombres de las variables, tipos de datos, dimensiones de arrays, comentarios, y, crucialmente, calcular el offset (desplazamiento) absoluto en bytes y bits para cada variable dentro de su DB o UDT padre. El resultado final del parsing es una representación estructurada (por ejemplo, en formato JSON) que puede ser utilizada para diversas aplicaciones, como la generación automática de documentación, la creación de interfaces HMI/SCADA, herramientas de diagnóstico, o la integración con otros sistemas. ## 2. Conceptos Clave de Step 7 Relevantes para el Parsing Antes de detallar el proceso de parsing, es fundamental comprender algunos conceptos de Step 7: ### 2.1. Tipos de Datos Primitivos y Tamaños El parser debe conocer el tamaño en bytes (y bits para BOOL) de los tipos de datos fundamentales de S7: * **BOOL**: 1 bit. * **BYTE**: 1 byte (8 bits). * **CHAR**: 1 byte (representación ASCII). * **WORD**: 2 bytes. * **INT**: 2 bytes (entero con signo). * **UINT**: 2 bytes (entero sin signo, TIA Portal). * **DWORD**: 4 bytes. * **DINT**: 4 bytes (entero con signo). * **UDINT**: 4 bytes (entero sin signo, TIA Portal). * **REAL**: 4 bytes (punto flotante, IEEE 754). * **S5TIME**: 2 bytes (formato de temporizador S5). * **TIME**: 4 bytes (formato de tiempo IEC). * **TIME_OF_DAY (TOD)**: 4 bytes. * **DATE**: 2 bytes. * **DATE_AND_TIME (DT)**: 8 bytes. * **STRING[N]**: N+2 bytes (1 byte max_len, 1 byte actual_len, N bytes para caracteres). * **LREAL, LINT, ULINT, LWORD, LTIME, LTIME_OF_DAY** (TIA Portal): Generalmente 8 bytes. ### 2.2. Tipos de Datos Compuestos * **STRUCT**: Una colección de variables (miembros) de diferentes tipos, agrupadas bajo un nombre. Los miembros se almacenan secuencialmente. * **UDT (User Defined Type)**: Un tipo de dato definido por el usuario, que internamente es un STRUCT. Permite reutilizar estructuras complejas. * **ARRAY [L..U] OF Type**: Una colección de elementos del mismo tipo (`Type`), indexados desde `L` (límite inferior) hasta `U` (límite superior). ### 2.3. Reglas de Alineación y Offsets Siemens S7 tiene reglas estrictas para la alineación de datos en memoria, lo cual afecta el cálculo de offsets: * **BOOL**: Se empaquetan en bytes. Ocho BOOLs pueden ocupar un solo byte. * **BYTE, CHAR**: Pueden comenzar en cualquier offset de byte. * **WORD (y tipos de 2 bytes)**: Deben comenzar en un offset de byte par (0, 2, 4,...). Si el offset actual es impar, se inserta un byte de relleno (padding) antes del WORD. * **DWORD (y tipos de 4 bytes)**: También deben comenzar en un offset de byte par. * **LWORD (y tipos de 8 bytes)**: También deben comenzar en un offset de byte par. * **STRUCT / UDT**: Como un todo, un STRUCT o UDT comienza en un offset de byte par. Su tamaño total también se redondea (padding) para que sea un número par de bytes. Los miembros internos siguen sus propias reglas de alineación relativas al inicio del STRUCT/UDT. * **STRING**: El tipo STRING (con sus 2 bytes de cabecera) se considera alineado a palabra (comienza en offset par). * **ARRAY**: Un array de tipos que requieren alineación (ej. `ARRAY OF INT`) comenzará en un offset alineado (par). Los elementos dentro del array se colocan consecutivamente según el tamaño del tipo base. El tamaño total de un array de tipos alineados a palabra también resultará en un tamaño par o se le añadirá padding al final si el siguiente elemento lo requiere. ### 2.4. Formato del Archivo de Texto Los archivos `.db` o `.udt` exportados como "fuente" (source) tienen una sintaxis similar a AWL/STL: * Bloques `TYPE "Nombre_UDT" ... END_TYPE` para UDTs. * Bloques `DATA_BLOCK "Nombre_DB" ... END_DATA_BLOCK` para DBs. * Declaraciones de variables: `NombreVar : TIPO_DATO [:= ValorInicial] [// Comentario]` * Estructuras: `STRUCT ... END_STRUCT`. * Arrays: `ARRAY [inferior..superior] OF TIPO_DATO`. * Sección `BEGIN ... END_DATA_BLOCK` (para DBs) contiene los valores iniciales efectivos. ## 3. Arquitectura del Parser El parser se estructura en varios componentes lógicos: ### 3.1. Lector de Archivo * Lee el archivo de entrada línea por línea. ### 3.2. Identificador de Bloques * Detecta el inicio y fin de bloques UDT (`TYPE ... END_TYPE`) y DB (`DATA_BLOCK ... END_DATA_BLOCK`). ### 3.3. Analizador de Declaraciones * Utiliza expresiones regulares (regex) para identificar y extraer información de: * Propiedades del bloque (FAMILY, VERSION, TITLE). * Declaraciones de variables (nombre, tipo, valor inicial, comentario). * Inicio y fin de `STRUCT`. * Declaraciones de `ARRAY`. * Maneja la recursividad de `STRUCT` anidados. ### 3.4. Calculadora de Offsets y Tamaños * Mantiene el `byte_offset` y `bit_offset` actual durante el parsing de un `STRUCT` (o el cuerpo principal de un DB/UDT). * Aplica las reglas de alineación de S7. * Consulta una tabla interna de tamaños de tipos de datos primitivos. * Para UDTs usados como tipo de dato, consulta la información de UDTs previamente parseados. * Calcula el tamaño total de `STRUCT`s, `UDT`s y `ARRAY`s. ### 3.5. Gestor de UDTs * Almacena las definiciones de UDTs parseadas en un diccionario o estructura similar. * Cuando una variable es de un tipo UDT, el parser consulta este gestor para obtener el tamaño y la estructura del UDT. ### 3.6. Analizador de la Sección `BEGIN` (para DBs) * Parsea las asignaciones de valores en la sección `BEGIN` para actualizar los valores iniciales de las variables del DB. ### 3.7. Estructuras de Datos Internas * Se utilizan clases o diccionarios para representar: * `VariableInfo`: Nombre, tipo, offset, tamaño, sub-miembros (si es STRUCT/UDT), dimensiones de array, etc. * `UdtInfo`: Nombre, lista de `VariableInfo` (miembros), tamaño total. * `DbInfo`: Nombre, lista de `VariableInfo` (miembros), tamaño total, valores de la sección `BEGIN`. * `ParsedData`: Contenedor raíz para todos los UDTs y DBs parseados. ### 3.8. Serializador de Salida * Convierte las estructuras de datos internas a formato JSON. ## 4. Proceso Detallado del Parsing El proceso general sigue estos pasos: 1. **Inicialización**: * Crear un objeto `ParsedData` para almacenar los resultados. * Crear un diccionario `known_udts` para almacenar las definiciones de UDTs a medida que se parsean. * Inicializar `current_byte_offset = 0`, `current_bit_offset = 0`. 2. **Lectura Línea por Línea**: * Para cada línea del archivo: * **Detección de Bloque UDT**: Si la línea marca el inicio de un UDT (`TYPE "Nombre"`): * Crear un nuevo objeto `UdtInfo`. * Parsear las propiedades del UDT (FAMILY, VERSION). * Resetear `current_byte_offset = 0`, `current_bit_offset = 0` para este UDT. * Llamar a una función recursiva/iterativa `parse_struct_members` para los miembros del UDT. * Al encontrar `END_TYPE`, finalizar el UDT, aplicar padding final si es necesario para que el tamaño total sea par, y almacenar el UDT en `known_udts` y `ParsedData.udts`. * **Detección de Bloque DB**: Si la línea marca el inicio de un DB (`DATA_BLOCK "Nombre"`): * Crear un nuevo objeto `DbInfo`. * Parsear las propiedades del DB (TITLE, FAMILY, VERSION). * Resetear `current_byte_offset = 0`, `current_bit_offset = 0` para este DB. * Llamar a `parse_struct_members` para los miembros del DB. * Si se encuentra `BEGIN`: * Finalizar el cálculo de offsets para la sección de declaración (aplicar padding si el offset es impar). * Llamar a una función `parse_begin_section` para extraer los valores iniciales. * Al encontrar `END_DATA_BLOCK`, finalizar el DB, aplicar padding final al tamaño total de las declaraciones si es necesario, y almacenar el DB en `ParsedData.dbs`. * **Dentro de `parse_struct_members(parent_members_list, current_offsets)`**: * Si la línea es `STRUCT`: * Alinear `current_offsets` a palabra (byte par). * Crear un `VariableInfo` para el `STRUCT` anónimo o nombrado. * Grabar el offset de inicio del `STRUCT`. * Hacer una llamada recursiva a `parse_struct_members` para los hijos del `STRUCT`, pasándole nuevos offsets relativos (0.0). * Al retornar de la llamada recursiva (al encontrar `END_STRUCT`): * El tamaño del `STRUCT` es el offset final retornado por la llamada recursiva (después de su propio padding final a par). * Actualizar `current_offsets` del nivel padre sumando el tamaño del `STRUCT`. * Añadir el `VariableInfo` del `STRUCT` a `parent_members_list`. * Si la línea es `END_STRUCT`: Retornar (indicando el fin de este nivel de `STRUCT`). * Si la línea es una **declaración de variable** (detectada por regex): * Extraer `nombre_var`, `tipo_completo` (ej. `ARRAY [0..9] OF INT`, `STRING[32]`, `"MiUDT"`), `valor_inicial`, `comentario`. * Analizar `tipo_completo` para determinar: * Si es `ARRAY`: extraer dimensiones y `tipo_base_array`. * Si es `STRING`: extraer `longitud_string`. * Si es un UDT (entre comillas): `nombre_udt_usado`. * Sino: `tipo_primitivo`. * Obtener `tamaño_unitario` y `alineacion_requerida_bytes` para el `tipo_base_final` (primitivo, UDT, STRING): * Para UDT: `tamaño_unitario = known_udts[nombre_udt_usado].total_size_in_bytes`. `alineacion_requerida_bytes = 2`. * Para STRING[N]: `tamaño_unitario = N + 2`. `alineacion_requerida_bytes = 2`. * Para primitivos: obtener de una tabla interna. * Crear un `VariableInfo`. * **Cálculo de Offset para la variable actual**: * Si el `tipo_base_final` es `BOOL`: * `VariableInfo.offset = current_offsets.byte + (current_offsets.bit / 10.0)` (representación decimal). * `current_offsets.bit += 1`. * Si `current_offsets.bit >= 8`: * `current_offsets.byte += 1`. * `current_offsets.bit = 0`. * El tamaño para `BOOL` es 1 bit. Si es un array de BOOLs, este proceso se repite para cada elemento. * Si el `tipo_base_final` **no** es `BOOL`: * Si `current_offsets.bit > 0` (se estaba empaquetando BOOLs): * `current_offsets.byte += 1` (terminar el byte actual). * `current_offsets.bit = 0`. * Si `alineacion_requerida_bytes == 2` y `current_offsets.byte` es impar: * `current_offsets.byte += 1` (insertar byte de padding). * `VariableInfo.offset = current_offsets.byte`. * `tamaño_total_var = tamaño_unitario`. * Si es `ARRAY`: `tamaño_total_var *= numero_total_elementos_array`. * `VariableInfo.size_in_bytes = tamaño_total_var`. * `current_offsets.byte += tamaño_total_var`. * Si es un array de elementos alineados a palabra y `tamaño_total_var` es impar (esto es raro, usualmente el `tamaño_unitario` ya es par), S7 no suele añadir padding *dentro* del array, sino que el padding general del STRUCT/UDT se encarga. La alineación es para el *inicio* del array y de cada elemento *si el tipo base lo requiere*. * Añadir el `VariableInfo` a `parent_members_list`. 3. **Post-Procesamiento**: * Una vez que todos los bloques han sido parseados, serializar el objeto `ParsedData` a JSON. ## 5. Expresiones Regulares Clave (Ejemplos Conceptuales) * **Inicio UDT**: `^\s*TYPE\s+"([^"]+)"` * **Inicio DB**: `^\s*DATA_BLOCK\s+"([^"]+)"` * **Propiedad**: `^\s*([A-Z_]+)\s*:\s*(.+?)\s*(?://.*)?$` * **Inicio STRUCT**: `^\s*STRUCT` * **Fin STRUCT/TYPE/DB**: `^\s*END_(STRUCT|TYPE|DATA_BLOCK)` * **Declaración de Variable (simplificada)**: `^\s*(?[a-zA-Z_]\w*)\s*:\s*(?(?:ARRAY\s*\[(?[^\]]+)\]\s*OF\s*)?(?(?:"[^"]+"|\w+))(?:\s*\[\s*(?\d+)\s*\])?)\s*(?::=\s*(?.+?))?\s*(?://\s*(?.*))?;?\s*$` * Esta regex es compleja y necesita ser probada y refinada cuidadosamente. Captura: * `name`: Nombre de la variable. * `typefull`: La declaración de tipo completa. * `arraydims`: Dimensiones del array (ej. `1..5,0..2`). * `basetype`: El tipo base (ej. `INT`, `"MiUDT"`, `STRING`). * `strlen`: Longitud para `STRING[N]`. * `initval`: Valor inicial. * `comment`: Comentario. ## 6. Estructura de Salida JSON (Ejemplo Conceptual) ```json { "udts": [ { "name": "Recipe_Prod", "family": "DataType", "version": "0.1", "total_size_in_bytes": 218, // Ejemplo, calculado "members": [ { "name": "_Name", "data_type": "STRING", "string_length": 32, "initial_value": "' '", "byte_offset": 0.0, "size_in_bytes": 34 }, { "name": "_EnProdTemp", "data_type": "BOOL", "byte_offset": 34.0, // Byte 34, bit 0 "size_in_bytes": 0 // Representa 1 bit }, // ... más miembros { "name": "_Type", "data_type": "INT", "initial_value": "1", "byte_offset": 40.0, // Ejemplo, después de padding y otros BOOLs/BYTEs "size_in_bytes": 2 } ] } ], "dbs": [ { "name": "HMI_Blender_Parameters", "title": "{ S7_language := '28(1) Albanese 15.06.2005 17:07:04' }", "family": "Resource", "version": "0.0", "total_size_in_bytes": 1024, // Ejemplo, calculado "members": [ { "name": "Processor_Options", "data_type": "STRUCT", "byte_offset": 0.0, "size_in_bytes": 100, // Ejemplo "children": [ { "name": "Blender_OPT", "data_type": "STRUCT", "byte_offset": 0.0, // Relativo a Processor_Options "size_in_bytes": 80, // Ejemplo "children": [ { "name": "_ModelNum", "data_type": "INT", "initial_value": "6", "byte_offset": 0.0, // Relativo a Blender_OPT "size_in_bytes": 2 } // ... ] } ] }, { "name": "Actual_Recipe_Parameters", "data_type": "UDT", // El parser identifica esto como UDT "udt_source_name": "Recipe_Prod", // Nombre del UDT original "byte_offset": 120.0, // Ejemplo "size_in_bytes": 218, // Tamaño del UDT "Recipe_Prod" "children": [] // Opcionalmente, se pueden expandir los miembros del UDT aquí } // ... más miembros ], "initial_values_from_begin_block": { "Processor_Options.Blender_OPT._ModelNum": "6", "Actual_Recipe_Parameters._Name": "''" // ... } } ] }