Compare commits

..

2 Commits

Author SHA1 Message Date
Miguel 0f162377cd S7_DB_Utils funcionando v1 2025-05-18 02:57:49 +02:00
Miguel e85c0c169d antes de eliminar las estructuras de Begin block de los json 2025-05-18 01:04:24 +02:00
12 changed files with 1347 additions and 791 deletions

View File

@ -1,15 +1,15 @@
--- Log de Ejecución: x3.py --- --- Log de Ejecución: x3.py ---
Grupo: S7_DB_Utils Grupo: S7_DB_Utils
Directorio de Trabajo: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001 Directorio de Trabajo: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001
Inicio: 2025-05-17 21:31:24 Inicio: 2025-05-18 02:09:01
Fin: 2025-05-17 21:31:25 Fin: 2025-05-18 02:09:01
Duración: 0:00:00.136451 Duración: 0:00:00.154928
Estado: SUCCESS (Código de Salida: 0) Estado: SUCCESS (Código de Salida: 0)
--- SALIDA ESTÁNDAR (STDOUT) --- --- SALIDA ESTÁNDAR (STDOUT) ---
Using working directory: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001 Using working directory: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001
Los archivos JSON de salida se guardarán en: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json Los archivos JSON de salida se guardarán en: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json
Archivos encontrados para procesar: 3 Archivos encontrados para procesar: 2
--- Procesando archivo: db1001_data.db --- --- Procesando archivo: db1001_data.db ---
Parseo completo. Intentando serializar a JSON: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json\db1001_data.json Parseo completo. Intentando serializar a JSON: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json\db1001_data.json
@ -19,10 +19,6 @@ Resultado guardado en: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giov
Parseo completo. Intentando serializar a JSON: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json\db1001_format.json Parseo completo. Intentando serializar a JSON: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json\db1001_format.json
Resultado guardado en: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json\db1001_format.json Resultado guardado en: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json\db1001_format.json
--- Procesando archivo: db1001_format_updated.db ---
Parseo completo. Intentando serializar a JSON: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json\db1001_format_updated.json
Resultado guardado en: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json\db1001_format_updated.json
--- Proceso completado --- --- Proceso completado ---
--- ERRORES (STDERR) --- --- ERRORES (STDERR) ---

View File

@ -1,34 +1,26 @@
--- Log de Ejecución: x4.py --- --- Log de Ejecución: x4.py ---
Grupo: S7_DB_Utils Grupo: S7_DB_Utils
Directorio de Trabajo: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001 Directorio de Trabajo: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001
Inicio: 2025-05-17 21:37:42 Inicio: 2025-05-18 02:13:16
Fin: 2025-05-17 21:37:42 Fin: 2025-05-18 02:13:16
Duración: 0:00:00.131741 Duración: 0:00:00.162328
Estado: SUCCESS (Código de Salida: 0) Estado: SUCCESS (Código de Salida: 0)
--- SALIDA ESTÁNDAR (STDOUT) --- --- SALIDA ESTÁNDAR (STDOUT) ---
Using working directory: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001 Using working directory: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001
Los archivos de documentación generados se guardarán en: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation Los archivos de documentación generados se guardarán en: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation
Archivos JSON encontrados para procesar: 3 Archivos JSON encontrados para procesar: 2
--- Procesando archivo JSON: db1001_data.json --- --- Procesando archivo JSON: db1001_data.json ---
Archivo JSON 'db1001_data.json' cargado correctamente. Archivo JSON 'db1001_data.json' cargado correctamente.
INFO: Usando '_begin_block_assignments_ordered' para generar bloque BEGIN de DB 'HMI_Blender_Parameters'.
Archivo S7 reconstruido generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_data.txt Archivo S7 reconstruido generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_data.txt
Archivo Markdown de documentación generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_data.md Archivo Markdown de documentación generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_data.md
--- Procesando archivo JSON: db1001_format.json --- --- Procesando archivo JSON: db1001_format.json ---
Archivo JSON 'db1001_format.json' cargado correctamente. Archivo JSON 'db1001_format.json' cargado correctamente.
INFO: Usando '_begin_block_assignments_ordered' para generar bloque BEGIN de DB 'HMI_Blender_Parameters'.
Archivo S7 reconstruido generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_format.txt Archivo S7 reconstruido generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_format.txt
Archivo Markdown de documentación generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_format.md Archivo Markdown de documentación generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_format.md
--- Procesando archivo JSON: db1001_format_updated.json ---
Archivo JSON 'db1001_format_updated.json' cargado correctamente.
INFO: Usando '_begin_block_assignments_ordered' para generar bloque BEGIN de DB 'HMI_Blender_Parameters'.
Archivo S7 reconstruido generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_format_updated.txt
Archivo Markdown de documentación generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_format_updated.md
--- Proceso de generación de documentación completado --- --- Proceso de generación de documentación completado ---
--- ERRORES (STDERR) --- --- ERRORES (STDERR) ---

View File

@ -1,15 +1,15 @@
--- Log de Ejecución: x5.py --- --- Log de Ejecución: x5.py ---
Grupo: S7_DB_Utils Grupo: S7_DB_Utils
Directorio de Trabajo: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001 Directorio de Trabajo: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001
Inicio: 2025-05-17 21:51:25 Inicio: 2025-05-18 02:19:47
Fin: 2025-05-17 21:51:25 Fin: 2025-05-18 02:19:47
Duración: 0:00:00.099104 Duración: 0:00:00.125156
Estado: SUCCESS (Código de Salida: 0) Estado: SUCCESS (Código de Salida: 0)
--- SALIDA ESTÁNDAR (STDOUT) --- --- SALIDA ESTÁNDAR (STDOUT) ---
Using working directory: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001 Using working directory: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001
Los archivos Markdown de descripción se guardarán en: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation Los archivos Markdown de descripción se guardarán en: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation
Archivos JSON encontrados para procesar: 3 Archivos JSON encontrados para procesar: 2
--- Procesando archivo JSON para descripción: db1001_data.json --- --- Procesando archivo JSON para descripción: db1001_data.json ---
Archivo JSON 'db1001_data.json' cargado correctamente. Archivo JSON 'db1001_data.json' cargado correctamente.
@ -19,10 +19,6 @@ Documentación Markdown completa generada: C:\Trabajo\SIDEL\09 - SAE452 - Diet a
Archivo JSON 'db1001_format.json' cargado correctamente. Archivo JSON 'db1001_format.json' cargado correctamente.
Documentación Markdown completa generada: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_format_description.md Documentación Markdown completa generada: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_format_description.md
--- Procesando archivo JSON para descripción: db1001_format_updated.json ---
Archivo JSON 'db1001_format_updated.json' cargado correctamente.
Documentación Markdown completa generada: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_format_updated_description.md
--- Proceso de generación de descripciones Markdown completado --- --- Proceso de generación de descripciones Markdown completado ---
--- ERRORES (STDERR) --- --- ERRORES (STDERR) ---

View File

@ -1,30 +1,25 @@
--- Log de Ejecución: x6.py --- --- Log de Ejecución: x6.py ---
Grupo: S7_DB_Utils Grupo: S7_DB_Utils
Directorio de Trabajo: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001 Directorio de Trabajo: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001
Inicio: 2025-05-17 22:05:32 Inicio: 2025-05-18 02:20:21
Fin: 2025-05-17 22:05:33 Fin: 2025-05-18 02:20:22
Duración: 0:00:00.614471 Duración: 0:00:01.130771
Estado: SUCCESS (Código de Salida: 0) Estado: SUCCESS (Código de Salida: 0)
--- SALIDA ESTÁNDAR (STDOUT) --- --- SALIDA ESTÁNDAR (STDOUT) ---
Using working directory: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001 Using working directory: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001
Los archivos Excel de documentación se guardarán en: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation Los archivos Excel de documentación se guardarán en: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation
Archivos JSON encontrados para procesar: 3 Archivos JSON encontrados para procesar: 2
--- Procesando archivo JSON para Excel: db1001_data.json --- --- Procesando archivo JSON para Excel: db1001_data.json ---
Archivo JSON 'db1001_data.json' cargado correctamente. Archivo JSON 'db1001_data.json' cargado correctamente.
Generando documentación Excel para DB: 'HMI_Blender_Parameters' (desde db1001_data.json) -> C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_data.json.xlsx Generando documentación Excel para DB: 'HMI_Blender_Parameters' (desde db1001_data.json) -> C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_data.json_HMI_Blender_Parameters.xlsx
Excel documentation generated: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_data.json.xlsx Excel documentation generated: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_data.json_HMI_Blender_Parameters.xlsx
--- Procesando archivo JSON para Excel: db1001_format.json --- --- Procesando archivo JSON para Excel: db1001_format.json ---
Archivo JSON 'db1001_format.json' cargado correctamente. Archivo JSON 'db1001_format.json' cargado correctamente.
Generando documentación Excel para DB: 'HMI_Blender_Parameters' (desde db1001_format.json) -> C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_format.json.xlsx Generando documentación Excel para DB: 'HMI_Blender_Parameters' (desde db1001_format.json) -> C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_format.json_HMI_Blender_Parameters.xlsx
Excel documentation generated: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_format.json.xlsx Excel documentation generated: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_format.json_HMI_Blender_Parameters.xlsx
--- Procesando archivo JSON para Excel: db1001_format_updated.json ---
Archivo JSON 'db1001_format_updated.json' cargado correctamente.
Generando documentación Excel para DB: 'HMI_Blender_Parameters' (desde db1001_format_updated.json) -> C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_format_updated.json.xlsx
Excel documentation generated: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_format_updated.json.xlsx
--- Proceso de generación de documentación Excel completado --- --- Proceso de generación de documentación Excel completado ---

View File

@ -1,28 +1,33 @@
--- Log de Ejecución: x7_value_updater.py --- --- Log de Ejecución: x7_value_updater.py ---
Grupo: S7_DB_Utils Grupo: S7_DB_Utils
Directorio de Trabajo: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001 Directorio de Trabajo: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001
Inicio: 2025-05-17 23:48:43 Inicio: 2025-05-18 02:56:24
Fin: 2025-05-17 23:48:43 Fin: 2025-05-18 02:56:25
Duración: 0:00:00.106052 Duración: 0:00:00.761362
Estado: SUCCESS (Código de Salida: 0) Estado: SUCCESS (Código de Salida: 0)
--- SALIDA ESTÁNDAR (STDOUT) --- --- SALIDA ESTÁNDAR (STDOUT) ---
Using working directory: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001 Using working directory: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001
Found _data file: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\db1001_data.db Los archivos JSON se guardarán en: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json
Found _format file: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\db1001_format.db Los archivos de documentación se guardarán en: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation
Parsing S7 file: db1001_data.db... Se encontraron 1 pares de archivos para procesar.
Serializing to JSON: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json\db1001_data_data.json
JSON saved: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json\db1001_data_data.json
Parsing S7 file: db1001_format.db...
Serializing to JSON: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json\db1001_format_format.json
JSON saved: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json\db1001_format_format.json
Comparing structure of DB: HMI_Blender_Parameters
La estructura del DB 'HMI_Blender_Parameters' es compatible.
All DB structures are compatible. Proceeding to generate _updated file. --- Procesando par de archivos ---
INFO: Usando '_begin_block_assignments_ordered' para generar bloque BEGIN de DB 'HMI_Blender_Parameters'. Data file: db1001_data.db
Format file: db1001_format.db
Parseando archivo data: db1001_data.db
Parseando archivo format: db1001_format.db
Archivos JSON generados: db1001_data.json y db1001_format.json
Comparando estructuras para DB 'HMI_Blender_Parameters': 284 variables en _data, 284 variables en _format
Successfully generated _updated S7 file: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\db1001_updated.db Los archivos son compatibles. Creando el archivo _updated...
Archivo _updated generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json\db1001_updated.json
Archivo de comparación Excel generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_comparison.xlsx
Archivo Markdown generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_updated.md
Archivo S7 generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_updated.txt
Archivo S7 copiado a: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\db1001_updated.db
--- Proceso completado ---
--- ERRORES (STDERR) --- --- ERRORES (STDERR) ---
Ninguno Ninguno

View File

@ -42,9 +42,9 @@
"hidden": false "hidden": false
}, },
"x7_value_updater.py": { "x7_value_updater.py": {
"display_name": "x7_value_updater", "display_name": "07: Actualizar Valores de DB (JSON)",
"short_description": "Sin descripción corta.", "short_description": "Busca archivos .db o .awl con la terminacion _data y _format. Si los encuentra y son compatibles usa los datos de _data para generar un _updated con los nombres de las variables de _format",
"long_description": "", "long_description": "Procesa pares de archivos a JSON (_data.json y _format.json, generados por x3.py). Compara sus estructuras por offset para asegurar compatibilidad. Si son compatibles, crea un nuevo archivo _updated.json que combina la estructura del _format.json con los valores actuales del _data.json.",
"hidden": false "hidden": false
} }
} }

View File

@ -1,10 +1,10 @@
# --- x3.py (Modificaciones v_final_4 - Incluye 'count' para ArrayDimension y ajuste debug) --- # --- x3_refactored.py ---
import re import re
import json import json
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import List, Dict, Optional, Union, Tuple, Any from typing import List, Dict, Optional, Union, Tuple, Any
import os # Asegurarse de que os está importado import os
import glob # Para buscar archivos import glob
import copy import copy
import sys import sys
@ -29,11 +29,11 @@ class ArrayDimension:
upper_bound: int upper_bound: int
@property @property
def count(self) -> int: # La propiedad 'count' se calculará def count(self) -> int:
return self.upper_bound - self.lower_bound + 1 return self.upper_bound - self.lower_bound + 1
@dataclass @dataclass
class VariableInfo: # Sin cambios respecto a v_final_3 class VariableInfo:
name: str name: str
data_type: str data_type: str
byte_offset: float byte_offset: float
@ -50,7 +50,7 @@ class VariableInfo: # Sin cambios respecto a v_final_3
current_element_values: Optional[Dict[str, str]] = None current_element_values: Optional[Dict[str, str]] = None
@dataclass @dataclass
class UdtInfo: # Sin cambios respecto a v_final_3 class UdtInfo:
name: str name: str
family: Optional[str] = None family: Optional[str] = None
version: Optional[str] = None version: Optional[str] = None
@ -58,23 +58,23 @@ class UdtInfo: # Sin cambios respecto a v_final_3
total_size_in_bytes: int = 0 total_size_in_bytes: int = 0
@dataclass @dataclass
class DbInfo: # Sin cambios respecto a v_final_3 class DbInfo:
name: str name: str
title: Optional[str] = None title: Optional[str] = None
family: Optional[str] = None family: Optional[str] = None
version: Optional[str] = None version: Optional[str] = None
members: List[VariableInfo] = field(default_factory=list) members: List[VariableInfo] = field(default_factory=list)
total_size_in_bytes: int = 0 total_size_in_bytes: int = 0
_begin_block_assignments_ordered: List[Tuple[str, str]] = field(default_factory=list) # Eliminamos los campos redundantes:
_initial_values_from_begin_block: Dict[str, str] = field(default_factory=dict) # _begin_block_assignments_ordered y _initial_values_from_begin_block
@dataclass @dataclass
class ParsedData: # Sin cambios class ParsedData:
udts: List[UdtInfo] = field(default_factory=list) udts: List[UdtInfo] = field(default_factory=list)
dbs: List[DbInfo] = field(default_factory=list) dbs: List[DbInfo] = field(default_factory=list)
@dataclass @dataclass
class OffsetContext: # Sin cambios class OffsetContext:
byte_offset: int = 0 byte_offset: int = 0
bit_offset: int = 0 bit_offset: int = 0
def get_combined_offset(self) -> float: def get_combined_offset(self) -> float:
@ -89,7 +89,7 @@ class OffsetContext: # Sin cambios
if self.byte_offset % 2 != 0: self.byte_offset += 1 if self.byte_offset % 2 != 0: self.byte_offset += 1
# --- Fin Estructuras de Datos --- # --- Fin Estructuras de Datos ---
S7_PRIMITIVE_SIZES = { # Sin cambios S7_PRIMITIVE_SIZES = {
"BOOL": (0, 1, True), "BYTE": (1, 1, False), "CHAR": (1, 1, False), "BOOL": (0, 1, True), "BYTE": (1, 1, False), "CHAR": (1, 1, False),
"SINT": (1, 1, False), "USINT": (1, 1, False), "WORD": (2, 2, False), "SINT": (1, 1, False), "USINT": (1, 1, False), "WORD": (2, 2, False),
"INT": (2, 2, False), "UINT": (2, 2, False), "S5TIME": (2, 2, False), "INT": (2, 2, False), "UINT": (2, 2, False), "S5TIME": (2, 2, False),
@ -100,7 +100,7 @@ S7_PRIMITIVE_SIZES = { # Sin cambios
"LWORD": (8, 2, False), "DATE_AND_TIME": (8, 2, False), "DT": (8, 2, False), "LWORD": (8, 2, False), "DATE_AND_TIME": (8, 2, False), "DT": (8, 2, False),
} }
class S7Parser: # Sin cambios en __init__ respecto a v_final_3 class S7Parser:
def __init__(self): def __init__(self):
self.parsed_data = ParsedData() self.parsed_data = ParsedData()
self.known_udts: Dict[str, UdtInfo] = {} self.known_udts: Dict[str, UdtInfo] = {}
@ -125,7 +125,7 @@ class S7Parser: # Sin cambios en __init__ respecto a v_final_3
) )
self.array_dim_regex = re.compile(r'(\d+)\s*\.\.\s*(\d+)') self.array_dim_regex = re.compile(r'(\d+)\s*\.\.\s*(\d+)')
def _get_type_details(self, type_name_raw_cleaned: str) -> Tuple[int, int, bool, str]: # Sin cambios def _get_type_details(self, type_name_raw_cleaned: str) -> Tuple[int, int, bool, str]:
type_name_upper = type_name_raw_cleaned.upper() type_name_upper = type_name_raw_cleaned.upper()
if type_name_upper in S7_PRIMITIVE_SIZES: if type_name_upper in S7_PRIMITIVE_SIZES:
size, align, is_bool = S7_PRIMITIVE_SIZES[type_name_upper] size, align, is_bool = S7_PRIMITIVE_SIZES[type_name_upper]
@ -138,7 +138,7 @@ class S7Parser: # Sin cambios en __init__ respecto a v_final_3
raise ValueError(f"Tipo de dato desconocido o UDT no definido: '{type_name_raw_cleaned}'") raise ValueError(f"Tipo de dato desconocido o UDT no definido: '{type_name_raw_cleaned}'")
@staticmethod @staticmethod
def _adjust_children_offsets(children: List[VariableInfo], base_offset_add: float): # Sin cambios def _adjust_children_offsets(children: List[VariableInfo], base_offset_add: float):
for child in children: for child in children:
child.byte_offset += base_offset_add child.byte_offset += base_offset_add
if child.byte_offset == float(int(child.byte_offset)): if child.byte_offset == float(int(child.byte_offset)):
@ -149,7 +149,7 @@ class S7Parser: # Sin cambios en __init__ respecto a v_final_3
def _parse_struct_members(self, lines: List[str], current_line_idx: int, def _parse_struct_members(self, lines: List[str], current_line_idx: int,
parent_members_list: List[VariableInfo], parent_members_list: List[VariableInfo],
active_context: OffsetContext, active_context: OffsetContext,
is_top_level_struct_in_block: bool = False) -> int: # Ajuste en depuración is_top_level_struct_in_block: bool = False) -> int:
idx_to_process = current_line_idx idx_to_process = current_line_idx
while idx_to_process < len(lines): while idx_to_process < len(lines):
original_line_text = lines[idx_to_process].strip() original_line_text = lines[idx_to_process].strip()
@ -178,11 +178,11 @@ class S7Parser: # Sin cambios en __init__ respecto a v_final_3
active_context.align_to_byte() active_context.align_to_byte()
if active_context.byte_offset % 2 != 0: active_context.byte_offset += 1 if active_context.byte_offset % 2 != 0: active_context.byte_offset += 1
return line_index_for_return return line_index_for_return
if is_main_block_end_struct: # Simplemente lo ignoramos aquí, será manejado por END_TYPE/DB if is_main_block_end_struct:
pass pass
var_match = self.var_regex_simplified.match(line_to_parse) var_match = self.var_regex_simplified.match(line_to_parse)
if var_match: # Lógica de var_match sin cambios respecto a v_final_3 if var_match:
var_data = var_match.groupdict() var_data = var_match.groupdict()
raw_base_type_from_regex = var_data['basetype'].strip() raw_base_type_from_regex = var_data['basetype'].strip()
clean_data_type = raw_base_type_from_regex.strip('"') clean_data_type = raw_base_type_from_regex.strip('"')
@ -244,54 +244,186 @@ class S7Parser: # Sin cambios en __init__ respecto a v_final_3
if expanded_member.children: S7Parser._adjust_children_offsets(expanded_member.children, udt_instance_abs_start_offset) if expanded_member.children: S7Parser._adjust_children_offsets(expanded_member.children, udt_instance_abs_start_offset)
var_info.children.append(expanded_member) var_info.children.append(expanded_member)
parent_members_list.append(var_info) parent_members_list.append(var_info)
# Ajuste de la condición del mensaje de depuración
elif line_to_parse and \ elif line_to_parse and \
not self.struct_start_regex.match(line_to_parse) and \ not self.struct_start_regex.match(line_to_parse) and \
not is_main_block_end_struct and \ not is_main_block_end_struct and \
not is_nested_end_struct and \ not is_nested_end_struct and \
not is_block_terminator : # Solo imprimir si no es un terminador conocido not is_block_terminator :
print(f"DEBUG (_parse_struct_members): Line not parsed: Original='{original_line_text}' | Processed='{line_to_parse}'") print(f"DEBUG (_parse_struct_members): Line not parsed: Original='{original_line_text}' | Processed='{line_to_parse}'")
return idx_to_process return idx_to_process
def _parse_begin_block(self, lines: List[str], start_idx: int, db_info: DbInfo) -> int: # Sin cambios def _parse_begin_block(self, lines: List[str], start_idx: int, db_info: DbInfo) -> int:
"""
Parsea el bloque BEGIN y aplica directamente los valores a las variables
correspondientes, calculando también offsets para elementos de arrays.
"""
idx = start_idx idx = start_idx
assignment_regex = re.compile(r'^\s*(?P<path>.+?)\s*:=\s*(?P<value>.+?)\s*;?\s*$', re.IGNORECASE) assignment_regex = re.compile(r'^\s*(?P<path>.+?)\s*:=\s*(?P<value>.+?)\s*;?\s*$', re.IGNORECASE)
# Diccionario temporal para mapear rutas a variables
path_to_var_map = {}
# Función para calcular offset de elemento de array
def calculate_array_element_offset(var: VariableInfo, indices_str: str) -> float:
# Parsear los índices (pueden ser múltiples para arrays multidimensionales)
indices = [int(idx.strip()) for idx in indices_str.split(',')]
# Obtener dimensiones del array
dimensions = var.array_dimensions
if not dimensions or len(indices) != len(dimensions):
return var.byte_offset # No podemos calcular, devolver offset base
# Determinar tamaño de cada elemento base
element_size = 0
is_bit_array = False
if var.data_type.upper() == "BOOL":
is_bit_array = True
element_size = 0.1 # 0.1 byte = 1 bit (representación decimal)
elif var.data_type.upper() == "STRING" and var.string_length is not None:
element_size = var.string_length + 2
else:
# Para tipos primitivos y UDTs
data_type_upper = var.data_type.upper()
if data_type_upper in S7_PRIMITIVE_SIZES:
element_size = S7_PRIMITIVE_SIZES[data_type_upper][0]
elif var.data_type in self.known_udts:
element_size = self.known_udts[var.data_type].total_size_in_bytes
else:
# Si no podemos determinar tamaño, usar tamaño total / elementos
total_elements = 1
for dim in dimensions:
total_elements *= dim.count
if total_elements > 0 and var.size_in_bytes > 0:
element_size = var.size_in_bytes / total_elements
# Calcular offset para arrays multidimensionales
# Necesitamos calcular el índice lineal basado en índices multidimensionales
linear_index = 0
dimension_multiplier = 1
# Calcular desde la dimensión más interna a la más externa
# Los índices en S7 comienzan en las dimensiones a la izquierda
for i in range(len(indices)-1, -1, -1):
# Ajustar por el índice inicial de cada dimensión
adjusted_index = indices[i] - dimensions[i].lower_bound
linear_index += adjusted_index * dimension_multiplier
# Multiplicador para la siguiente dimensión
if i > 0: # No es necesario para la última iteración
dimension_multiplier *= dimensions[i].count
# Para arrays de bits, tenemos que calcular bit por bit
if is_bit_array:
base_byte = int(var.byte_offset)
base_bit = int(round((var.byte_offset - base_byte) * 10))
# Calcular nuevo bit y byte
new_bit = base_bit + linear_index
new_byte = base_byte + (new_bit // 8)
new_bit_position = new_bit % 8
return float(new_byte) + (float(new_bit_position) / 10.0)
else:
# Para tipos regulares, simplemente sumamos el offset lineal
return var.byte_offset + (linear_index * element_size)
# Construir mapa de rutas a variables
def build_path_map(members: List[VariableInfo], prefix: str = ""):
for var in members:
var_path = f"{prefix}{var.name}"
path_to_var_map[var_path] = var
# Para arrays, inicializar diccionario de elementos si es necesario
if var.array_dimensions:
var.current_element_values = {}
# Para variables con hijos, procesar recursivamente
if var.children:
build_path_map(var.children, f"{var_path}.")
# Construir el mapa antes de procesar el bloque BEGIN
build_path_map(db_info.members)
# Ahora procesar el bloque BEGIN
while idx < len(lines): while idx < len(lines):
original_line = lines[idx].strip(); line_to_parse = original_line original_line = lines[idx].strip()
line_to_parse = original_line
comment_marker = original_line.find("//") comment_marker = original_line.find("//")
if comment_marker != -1: line_to_parse = original_line[:comment_marker].strip() if comment_marker != -1:
if self.end_db_regex.match(line_to_parse): return idx line_to_parse = original_line[:comment_marker].strip()
if self.end_db_regex.match(line_to_parse):
break
idx += 1 idx += 1
if not line_to_parse: continue if not line_to_parse:
continue
match = assignment_regex.match(line_to_parse) match = assignment_regex.match(line_to_parse)
if match: if match:
path, value = match.group("path").strip(), match.group("value").strip().rstrip(';').strip() path, value = match.group("path").strip(), match.group("value").strip().rstrip(';').strip()
db_info._begin_block_assignments_ordered.append((path, value))
db_info._initial_values_from_begin_block[path] = value
raise SyntaxError("Se esperaba END_DATA_BLOCK después de la sección BEGIN.")
def _apply_current_values(self, members: List[VariableInfo], begin_values: Dict[str, str], current_path_prefix: str = ""): # Sin cambios # Distinguir entre asignación a elemento de array y variable normal
for var_info in members: if '[' in path and ']' in path:
full_member_path = f"{current_path_prefix}{var_info.name}" # Es un elemento de array
if var_info.array_dimensions: array_path = path[:path.find('[')]
var_info.current_element_values = {} indices = path[path.find('[')+1:path.find(']')]
prefix_for_search = full_member_path + "["
for key_in_begin, val_in_begin in begin_values.items():
if key_in_begin.startswith(prefix_for_search) and key_in_begin.endswith("]"):
try:
indices_str = key_in_begin[len(prefix_for_search):-1]
var_info.current_element_values[indices_str] = val_in_begin
except: print(f"Advertencia: No se pudo parsear el índice para: {key_in_begin}")
if not var_info.current_element_values: var_info.current_element_values = None
if full_member_path in begin_values: var_info.current_value = begin_values[full_member_path]
elif full_member_path in begin_values: var_info.current_value = begin_values[full_member_path]
elif var_info.initial_value is not None: var_info.current_value = var_info.initial_value
if var_info.children and not var_info.is_udt_expanded_member:
self._apply_current_values(var_info.children, begin_values, f"{full_member_path}.")
elif var_info.udt_source_name and var_info.children:
self._apply_current_values(var_info.children, begin_values, f"{full_member_path}.")
def parse_file(self, filepath: str) -> ParsedData: # Sin cambios respecto a v_final_3 if array_path in path_to_var_map:
var = path_to_var_map[array_path]
if var.current_element_values is None:
var.current_element_values = {}
# Calcular y guardar el offset real del elemento
element_offset = calculate_array_element_offset(var, indices)
# Guardar como un objeto con valor y offset
var.current_element_values[indices] = {
"value": value,
"offset": element_offset
}
elif path in path_to_var_map:
# Es una variable normal (o array completo)
var = path_to_var_map[path]
var.current_value = value
# También manejar rutas jerárquicas (e.g., MyStruct.MyField)
if '.' in path and '[' not in path: # Para simplificar, excluimos arrays con path jerárquico
parts = path.split('.')
current_path = ""
current_var = None
# Navegar por la jerarquía
for i, part in enumerate(parts):
if current_path:
current_path += f".{part}"
else:
current_path = part
if current_path in path_to_var_map:
current_var = path_to_var_map[current_path]
# Si es el último componente, asignar valor
if i == len(parts) - 1 and current_var:
current_var.current_value = value
# Propagar valores iniciales a variables sin asignación explícita
def propagate_initial_values(members: List[VariableInfo]):
for var in members:
# Si no tiene current_value pero tiene initial_value, copiar
if var.current_value is None and var.initial_value is not None:
var.current_value = var.initial_value
# Recursión para hijos
if var.children:
propagate_initial_values(var.children)
# Propagar valores iniciales
propagate_initial_values(db_info.members)
return idx
def parse_file(self, filepath: str) -> ParsedData:
try: try:
with open(filepath, 'r', encoding='utf-8-sig') as f: lines = f.readlines() with open(filepath, 'r', encoding='utf-8-sig') as f: lines = f.readlines()
except Exception as e: print(f"Error al leer el archivo {filepath}: {e}"); return self.parsed_data except Exception as e: print(f"Error al leer el archivo {filepath}: {e}"); return self.parsed_data
@ -334,42 +466,260 @@ class S7Parser: # Sin cambios en __init__ respecto a v_final_3
elif self.end_type_regex.match(line_to_parse) and isinstance(current_block_handler, UdtInfo): elif self.end_type_regex.match(line_to_parse) and isinstance(current_block_handler, UdtInfo):
if current_block_handler.total_size_in_bytes == 0: current_block_handler.total_size_in_bytes = active_block_context.byte_offset if current_block_handler.total_size_in_bytes == 0: current_block_handler.total_size_in_bytes = active_block_context.byte_offset
self.known_udts[current_block_handler.name] = current_block_handler self.known_udts[current_block_handler.name] = current_block_handler
# print(f"Parsed UDT: {current_block_handler.name}, Size: {current_block_handler.total_size_in_bytes}b, Members: {len(current_block_handler.members)}")
current_block_handler = None; parsing_title_value_next_line = False current_block_handler = None; parsing_title_value_next_line = False
elif self.end_db_regex.match(line_to_parse) and isinstance(current_block_handler, DbInfo): elif self.end_db_regex.match(line_to_parse) and isinstance(current_block_handler, DbInfo):
if current_block_handler.total_size_in_bytes == 0 : current_block_handler.total_size_in_bytes = active_block_context.byte_offset if current_block_handler.total_size_in_bytes == 0 : current_block_handler.total_size_in_bytes = active_block_context.byte_offset
self._apply_current_values(current_block_handler.members, current_block_handler._initial_values_from_begin_block) # Ya no necesitamos aplicar valores, porque se aplican directamente en _parse_begin_block
# print(f"Parsed DB: {current_block_handler.name}, Decl.Size: {current_block_handler.total_size_in_bytes}b, Members: {len(current_block_handler.members)}, BEGIN assigns: {len(current_block_handler._begin_block_assignments_ordered)}")
current_block_handler = None; parsing_title_value_next_line = False current_block_handler = None; parsing_title_value_next_line = False
idx += 1 idx += 1
return self.parsed_data return self.parsed_data
def custom_json_serializer(obj: Any) -> Any: def custom_json_serializer(obj: Any) -> Any:
if isinstance(obj, OffsetContext): return None if isinstance(obj, OffsetContext): return None
# Manejar ArrayDimension explícitamente para incluir 'count'
if isinstance(obj, ArrayDimension): if isinstance(obj, ArrayDimension):
return { return {
'lower_bound': obj.lower_bound, 'lower_bound': obj.lower_bound,
'upper_bound': obj.upper_bound, 'upper_bound': obj.upper_bound,
'count': obj.count # La propiedad se calcula y se añade aquí 'count': obj.count
} }
if hasattr(obj, '__dict__'): if hasattr(obj, '__dict__'):
d = {k: v for k, v in obj.__dict__.items() d = {k: v for k, v in obj.__dict__.items()
if not (v is None or (isinstance(v, list) and not v))} # No filtrar _initial_values_from_begin_block if not (v is None or (isinstance(v, list) and not v))}
if isinstance(obj, VariableInfo): if isinstance(obj, VariableInfo):
if not obj.is_udt_expanded_member and 'is_udt_expanded_member' not in d : if not obj.is_udt_expanded_member and 'is_udt_expanded_member' not in d:
d['is_udt_expanded_member'] = False d['is_udt_expanded_member'] = False
if not obj.current_element_values and 'current_element_values' in d:
del d['current_element_values'] # Manejar current_element_values con format especial para offsets
if isinstance(obj, DbInfo): # Asegurar que las listas vacías no se omitan si el campo existe if 'current_element_values' in d:
if '_begin_block_assignments_ordered' not in d and obj._begin_block_assignments_ordered == []: if not d['current_element_values']:
d['_begin_block_assignments_ordered'] = [] # Mantener lista vacía si es el caso del d['current_element_values']
if '_initial_values_from_begin_block' not in d and obj._initial_values_from_begin_block == {}: else:
d['_initial_values_from_begin_block'] = {} # Mantener dict vacío si es el caso # Asegurar que current_element_values se serializa correctamente
element_values = d['current_element_values']
if isinstance(element_values, dict):
# Preservar el formato {índice: {value, offset}}
d['current_element_values'] = element_values
return d return d
raise TypeError(f"Object of type {obj.__class__.__name__} is not JSON serializable: {type(obj)}") raise TypeError(f"Object of type {obj.__class__.__name__} is not JSON serializable: {type(obj)}")
def format_address_for_display(byte_offset: float, bit_size: int = 0) -> str:
"""
Formatea correctamente la dirección para mostrar, preservando el índice del bit para BOOLs.
Args:
byte_offset: El offset en bytes (con parte decimal para bits)
bit_size: Tamaño en bits (>0 para BOOLs)
Returns:
String formateado como "X.Y" para bits o "X" para bytes completos
"""
if bit_size > 0:
# Para BOOL, extraer y mostrar el byte y bit exactos
byte_part = int(byte_offset)
# Multiplicamos por 10 y tomamos el entero para obtener el índice correcto del bit
bit_part = int(round((byte_offset - byte_part) * 10))
return f"{byte_part}.{bit_part}"
else:
# Para otros tipos, mostrar como entero si es un byte completo
if byte_offset == float(int(byte_offset)):
return str(int(byte_offset))
return f"{byte_offset:.1f}"
def compare_offsets(offset1: float, offset2: float) -> int:
"""
Compara dos offsets considerando tanto la parte del byte como la del bit.
Returns:
-1 si offset1 < offset2, 0 si son iguales, 1 si offset1 > offset2
"""
# Extraer partes de byte y bit
byte1 = int(offset1)
bit1 = int(round((offset1 - byte1) * 10))
byte2 = int(offset2)
bit2 = int(round((offset2 - byte2) * 10))
# Comparar primero por byte
if byte1 < byte2:
return -1
elif byte1 > byte2:
return 1
# Si bytes son iguales, comparar por bit
if bit1 < bit2:
return -1
elif bit1 > bit2:
return 1
# Son exactamente iguales
return 0
def calculate_array_element_offset(var: VariableInfo, indices_str: str) -> float:
"""
Calcula el offset exacto para un elemento de array basado en sus índices.
Maneja correctamente arrays de bits y multidimensionales.
Args:
var: Variable información del array
indices_str: String de índices (e.g. "1,2" para array bidimensional)
Returns:
Offset calculado como float, con parte decimal para bits
"""
# Parsear los índices (pueden ser múltiples para arrays multidimensionales)
indices = [int(idx.strip()) for idx in indices_str.split(',')]
# Obtener dimensiones del array
dimensions = var.array_dimensions
if not dimensions or len(indices) != len(dimensions):
return var.byte_offset # No podemos calcular, devolver offset base
# Determinar tamaño de cada elemento base
element_size = 0
is_bit_array = False
if var.data_type.upper() == "BOOL":
is_bit_array = True
element_size = 0.1 # 0.1 byte = 1 bit (representación decimal)
elif var.data_type.upper() == "STRING" and var.string_length is not None:
element_size = var.string_length + 2 # Para strings, sumar 2 bytes de cabecera
else:
# Para tipos primitivos y UDTs
data_type_upper = var.data_type.upper()
if data_type_upper in S7_PRIMITIVE_SIZES:
element_size = S7_PRIMITIVE_SIZES[data_type_upper][0]
elif var.data_type in self.known_udts:
element_size = self.known_udts[var.data_type].total_size_in_bytes
else:
# Si no podemos determinar tamaño, usar tamaño total / elementos
total_elements = 1
for dim in dimensions:
total_elements *= dim.count
if total_elements > 0 and var.size_in_bytes > 0:
element_size = var.size_in_bytes / total_elements
# Calcular offset para arrays multidimensionales
# En S7, los arrays se almacenan en orden Row-Major (la última dimensión varía más rápido)
linear_index = 0
dimension_multiplier = 1
# Calcular desde la dimensión más interna a la más externa
# Para S7, procesamos desde la última dimensión hacia la primera
for i in range(len(indices)-1, -1, -1):
# Ajustar por el índice inicial de cada dimensión
adjusted_index = indices[i] - dimensions[i].lower_bound
linear_index += adjusted_index * dimension_multiplier
# Multiplicador para la siguiente dimensión
if i > 0: # No es necesario para la última iteración
dimension_multiplier *= dimensions[i].count
# Calcular offset según tipo
if is_bit_array:
# Para arrays de bits, calcular bit por bit
base_byte = int(var.byte_offset)
base_bit = int(round((var.byte_offset - base_byte) * 10))
# Calcular nuevo bit y byte
new_bit = base_bit + linear_index
new_byte = base_byte + (new_bit // 8)
new_bit_position = new_bit % 8
# Formato S7: byte.bit con bit de 0-7
return float(new_byte) + (float(new_bit_position) / 10.0)
else:
# Para tipos regulares, simplemente sumar el offset lineal * tamaño elemento
return var.byte_offset + (linear_index * element_size)
def flatten_db_structure(db_info: Dict[str, Any]) -> List[Dict[str, Any]]:
"""
Función genérica que aplana completamente una estructura de DB/UDT,
expandiendo todas las variables anidadas, UDTs y elementos de array.
Garantiza ordenamiento estricto por offset (byte.bit).
Returns:
List[Dict]: Lista de variables aplanadas con todos sus atributos
y un path completo, ordenada por offset estricto.
"""
flat_variables = []
processed_ids = set() # Para evitar duplicados
def process_variable(var: Dict[str, Any], path_prefix: str = "", is_expansion: bool = False):
# Identificador único para esta variable en este contexto
var_id = f"{path_prefix}{var['name']}_{var['byte_offset']}"
# Evitar procesar duplicados (como miembros expandidos de UDTs)
if is_expansion and var_id in processed_ids:
return
if is_expansion:
processed_ids.add(var_id)
# Crear copia de la variable con path completo
flat_var = var.copy()
flat_var["full_path"] = f"{path_prefix}{var['name']}"
flat_var["is_array_element"] = False # Por defecto no es elemento de array
# Determinar si es array con valores específicos
is_array = bool(var.get("array_dimensions"))
has_array_values = is_array and var.get("current_element_values")
# Si no es un array con valores específicos, agregar la variable base
if not has_array_values:
# Asegurarse de que el offset esté en el formato correcto
flat_var["address_display"] = format_address_for_display(var["byte_offset"], var.get("bit_size", 0))
flat_variables.append(flat_var)
# Si es array con valores específicos, expandir cada elemento como variable individual
if has_array_values:
for idx, element_data in var.get("current_element_values", {}).items():
# Extraer valor y offset del elemento
if isinstance(element_data, dict) and "value" in element_data and "offset" in element_data:
# Nuevo formato con offset calculado
value = element_data["value"]
element_offset = element_data["offset"]
else:
# Compatibilidad con formato antiguo
value = element_data
element_offset = var["byte_offset"] # Offset base
# Crear una entrada por cada elemento del array
array_element = var.copy()
array_element["full_path"] = f"{path_prefix}{var['name']}[{idx}]"
array_element["is_array_element"] = True
array_element["array_index"] = idx
array_element["current_value"] = value
array_element["byte_offset"] = element_offset # Usar offset calculado
array_element["address_display"] = format_address_for_display(element_offset, var.get("bit_size", 0))
# Eliminar current_element_values para evitar redundancia
if "current_element_values" in array_element:
del array_element["current_element_values"]
flat_variables.append(array_element)
# Procesar recursivamente todos los hijos
if var.get("children"):
for child in var.get("children", []):
process_variable(
child,
f"{path_prefix}{var['name']}.",
is_expansion=bool(var.get("udt_source_name"))
)
# Procesar todos los miembros desde el nivel superior
for member in db_info.get("members", []):
process_variable(member)
# Ordenar estrictamente por offset byte.bit
flat_variables.sort(key=lambda x: (
int(x["byte_offset"]),
int(round((x["byte_offset"] - int(x["byte_offset"])) * 10))
))
return flat_variables
if __name__ == "__main__": if __name__ == "__main__":
working_dir = find_working_directory() working_dir = find_working_directory()
print(f"Using working directory: {working_dir}") print(f"Using working directory: {working_dir}")
@ -388,7 +738,7 @@ if __name__ == "__main__":
print(f"Archivos encontrados para procesar: {len(all_source_files)}") print(f"Archivos encontrados para procesar: {len(all_source_files)}")
for filepath in all_source_files: for filepath in all_source_files:
parser = S7Parser() # Nueva instancia para cada archivo para evitar estados residuales parser = S7Parser()
filename = os.path.basename(filepath) filename = os.path.basename(filepath)
print(f"\n--- Procesando archivo: {filename} ---") print(f"\n--- Procesando archivo: {filename} ---")

View File

@ -1,9 +1,10 @@
# --- x4.py (Modificaciones v_final_2) --- # --- x4_refactored.py ---
import json import json
from typing import List, Dict, Any from typing import List, Dict, Any
import sys import sys
import os import os
import glob # Para buscar archivos JSON import glob
from x3 import flatten_db_structure
script_root = os.path.dirname( script_root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(__file__))) os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
@ -19,7 +20,6 @@ def find_working_directory():
sys.exit(1) sys.exit(1)
return working_directory return working_directory
# format_data_type_for_source (sin cambios respecto a la v5 que te di antes)
def format_data_type_for_source(var_info: Dict[str, Any]) -> str: def format_data_type_for_source(var_info: Dict[str, Any]) -> str:
base_type = var_info.get("udt_source_name") if var_info.get("udt_source_name") else var_info["data_type"] base_type = var_info.get("udt_source_name") if var_info.get("udt_source_name") else var_info["data_type"]
type_str = "" type_str = ""
@ -45,7 +45,7 @@ def generate_variable_declaration_for_source(var_info: Dict[str, Any], indent_le
is_multiline_struct_def = (var_info["data_type"].upper() == "STRUCT" and \ is_multiline_struct_def = (var_info["data_type"].upper() == "STRUCT" and \
not var_info.get("udt_source_name") and \ not var_info.get("udt_source_name") and \
var_info.get("children")) var_info.get("children"))
if not is_multiline_struct_def: # Solo añadir ; si no es una cabecera de STRUCT multilínea if not is_multiline_struct_def:
line += ';' line += ';'
if var_info.get("comment"): if var_info.get("comment"):
@ -60,32 +60,76 @@ def generate_struct_members_for_source(members: List[Dict[str, Any]], indent_lev
not var_info.get("udt_source_name") and \ not var_info.get("udt_source_name") and \
var_info.get("children"): var_info.get("children"):
current_indent_str = " " * indent_level current_indent_str = " " * indent_level
lines.append(f'{current_indent_str}{var_info["name"]} : STRUCT') # SIN ; lines.append(f'{current_indent_str}{var_info["name"]} : STRUCT')
lines.extend(generate_struct_members_for_source(var_info["children"], indent_level + 1)) lines.extend(generate_struct_members_for_source(var_info["children"], indent_level + 1))
lines.append(f'{current_indent_str}END_STRUCT;') # CON ; lines.append(f'{current_indent_str}END_STRUCT;')
else: else:
lines.append(generate_variable_declaration_for_source(var_info, indent_level)) lines.append(generate_variable_declaration_for_source(var_info, indent_level))
return lines return lines
def generate_begin_block_assignments(db_info: Dict[str, Any], indent_level: int) -> List[str]: def generate_begin_block_assignments(db_info: Dict[str, Any], indent_level: int) -> List[str]:
"""
Genera asignaciones del bloque BEGIN para todas las variables con valores actuales,
ordenadas estrictamente por offset (byte.bit).
"""
indent_str = " " * indent_level indent_str = " " * indent_level
lines = [] lines = []
# Usar la lista ordenada de asignaciones del JSON, que x3.py ahora debería poblar
ordered_assignments = db_info.get("_begin_block_assignments_ordered")
if ordered_assignments and isinstance(ordered_assignments, list): # Obtener todas las variables aplanadas y ordenadas
print(f"INFO: Usando '_begin_block_assignments_ordered' para generar bloque BEGIN de DB '{db_info['name']}'.") flat_vars = flatten_db_structure(db_info)
for path, value_obj in ordered_assignments:
value_str = str(value_obj) # Para cada variable en el orden correcto, generar la asignación
for var in flat_vars:
# Verificar que tenga un valor actual para asignar
if var.get("current_value") is not None:
value_str = str(var["current_value"])
# Convertir valores booleanos a TRUE/FALSE según estándar S7
if value_str.lower() == "true": value_str = "TRUE" if value_str.lower() == "true": value_str = "TRUE"
elif value_str.lower() == "false": value_str = "FALSE" elif value_str.lower() == "false": value_str = "FALSE"
lines.append(f"{indent_str}{path} := {value_str};") # Asignaciones siempre con ;
else: # Generar la línea de asignación
print(f"ADVERTENCIA: '_begin_block_assignments_ordered' no encontrado para DB '{db_info['name']}'. " lines.append(f"{indent_str}{var['full_path']} := {value_str};")
"El bloque BEGIN puede estar incompleto o desordenado si se usa el fallback.")
# (Aquí podría ir el fallback a _generate_assignments_recursive_from_current_values si se desea) return lines
# fallback_lines = _generate_assignments_recursive_from_current_values(db_info.get("members", []), "", indent_str)
# if fallback_lines: lines.extend(fallback_lines) def generate_markdown_table(db_info: Dict[str, Any]) -> List[str]:
"""
Genera una tabla markdown completa con offsets de bits correctos.
"""
lines = []
lines.append(f"## Documentación para DB: {db_info['name']}")
lines.append("")
lines.append("| Address | Name | Type | Initial Value | Actual Value | Comment |")
lines.append("|---|---|---|---|---|---|")
# Obtener todas las variables aplanadas (ya ordenadas por offset)
flat_vars = flatten_db_structure(db_info)
# Mostrar todas las variables, incluyendo elementos de array
for var in flat_vars:
# Usar el address_display pre-calculado
address = var["address_display"]
name_for_display = var["full_path"]
# Formatear tipo adecuadamente según sea variable normal o elemento de array
if var.get("is_array_element"):
# Para elementos de array, mostrar solo el tipo base
if "array_dimensions" in var:
# Si todavía tenemos información de array, eliminar la parte ARRAY[..]
base_type = var["data_type"]
data_type_str = base_type
else:
data_type_str = var["data_type"]
else:
# Para variables normales, mostrar tipo completo
data_type_str = format_data_type_for_source(var)
# Formatear valores para la tabla
initial_value = str(var.get("initial_value", "")).replace("|", "\\|").replace("\n", " ")
actual_value = str(var.get("current_value", "")).replace("|", "\\|").replace("\n", " ")
comment = str(var.get("comment", "")).replace("|", "\\|").replace("\n", " ")
lines.append(f"| {address} | {name_for_display} | {data_type_str} | {initial_value} | {actual_value} | {comment} |")
return lines return lines
@ -93,70 +137,35 @@ def generate_s7_source_code_lines(data: Dict[str, Any]) -> List[str]:
lines = [] lines = []
for udt in data.get("udts", []): for udt in data.get("udts", []):
lines.append(f'TYPE "{udt["name"]}"') lines.append(f'TYPE "{udt["name"]}"')
if udt.get("family"): lines.append(f' FAMILY : {udt["family"]}') # SIN ; if udt.get("family"): lines.append(f' FAMILY : {udt["family"]}')
if udt.get("version"): lines.append(f' VERSION : {udt["version"]}') # SIN ; if udt.get("version"): lines.append(f' VERSION : {udt["version"]}')
lines.append("") lines.append("")
lines.append(" STRUCT") # SIN ; lines.append(" STRUCT")
lines.extend(generate_struct_members_for_source(udt["members"], 2)) lines.extend(generate_struct_members_for_source(udt["members"], 2))
lines.append(" END_STRUCT;") # CON ; lines.append(" END_STRUCT;")
lines.append(f'END_TYPE') # SIN ; según tu último comentario lines.append(f'END_TYPE')
lines.append("") lines.append("")
for db in data.get("dbs", []): for db in data.get("dbs", []):
lines.append(f'DATA_BLOCK "{db["name"]}"') lines.append(f'DATA_BLOCK "{db["name"]}"')
if db.get("title"): # TITLE = { ... } va tal cual y SIN ; if db.get("title"):
lines.append(f' TITLE = {db["title"]}') lines.append(f' TITLE = {db["title"]}')
if db.get("family"): lines.append(f' FAMILY : {db["family"]}') # SIN ; if db.get("family"): lines.append(f' FAMILY : {db["family"]}')
if db.get("version"): lines.append(f' VERSION : {db["version"]}') # SIN ; if db.get("version"): lines.append(f' VERSION : {db["version"]}')
lines.append("") lines.append("")
lines.append(" STRUCT") # SIN ; lines.append(" STRUCT")
lines.extend(generate_struct_members_for_source(db["members"], 2)) lines.extend(generate_struct_members_for_source(db["members"], 2))
lines.append(" END_STRUCT;") # CON ; lines.append(" END_STRUCT;")
begin_assignments = generate_begin_block_assignments(db, 1) # Indentación 1 para las asignaciones begin_assignments = generate_begin_block_assignments(db, 1)
if begin_assignments: if begin_assignments:
lines.append("BEGIN") # SIN ; lines.append("BEGIN")
lines.extend(begin_assignments) lines.extend(begin_assignments)
lines.append(f'END_DATA_BLOCK') # SIN ; según tu último comentario lines.append(f'END_DATA_BLOCK')
lines.append("") lines.append("")
return lines return lines
# generate_markdown_table (sin cambios respecto a la v5)
def generate_markdown_table(db_info: Dict[str, Any]) -> List[str]:
lines = []
lines.append(f"## Documentación para DB: {db_info['name']}") # Cambiado a H2 para múltiples DBs por archivo
lines.append("")
lines.append("| Address | Name | Type | Initial Value | Actual Value | Comment |")
lines.append("|---|---|---|---|---|---|")
processed_expanded_members = set()
def flatten_members_for_markdown(members: List[Dict[str, Any]], prefix: str = "", base_offset: float = 0.0, is_expansion: bool = False):
md_lines = []
for var_idx, var in enumerate(members):
member_id = f"{prefix}{var['name']}_{var_idx}"
if is_expansion and member_id in processed_expanded_members: continue
if is_expansion: processed_expanded_members.add(member_id)
name_for_display = f"{prefix}{var['name']}"
address = f"{var['byte_offset']:.1f}" if isinstance(var['byte_offset'], float) else str(var['byte_offset'])
if var.get("bit_size", 0) > 0 and isinstance(var['byte_offset'], float) and var['byte_offset'] != int(var['byte_offset']): pass
elif var.get("bit_size", 0) > 0 : address = f"{int(var['byte_offset'])}.0"
data_type_str = format_data_type_for_source(var)
initial_value = str(var.get("initial_value", "")).replace("|", "\\|").replace("\n", " ")
actual_value = str(var.get("current_value", "")).replace("|", "\\|").replace("\n", " ")
comment = str(var.get("comment", "")).replace("|", "\\|").replace("\n", " ")
is_struct_container = var["data_type"].upper() == "STRUCT" and not var.get("udt_source_name") and var.get("children")
is_udt_instance_container = bool(var.get("udt_source_name")) and var.get("children")
if not is_struct_container and not is_udt_instance_container or var.get("is_udt_expanded_member"):
md_lines.append(f"| {address} | {name_for_display} | {data_type_str} | {initial_value} | {actual_value} | {comment} |")
if var.get("children"):
md_lines.extend(flatten_members_for_markdown(var["children"],
f"{name_for_display}.",
var['byte_offset'],
is_expansion=bool(var.get("udt_source_name"))))
return md_lines
lines.extend(flatten_members_for_markdown(db_info.get("members", [])))
return lines
def main(): def main():
working_dir = find_working_directory() working_dir = find_working_directory()
print(f"Using working directory: {working_dir}") print(f"Using working directory: {working_dir}")
@ -188,7 +197,7 @@ def main():
print(f"Archivo JSON '{current_json_filename}' cargado correctamente.") print(f"Archivo JSON '{current_json_filename}' cargado correctamente.")
except Exception as e: except Exception as e:
print(f"Error al cargar/leer {current_json_filename}: {e}") print(f"Error al cargar/leer {current_json_filename}: {e}")
continue # Saltar al siguiente archivo JSON continue
# Generar archivo S7 (.txt) # Generar archivo S7 (.txt)
s7_code_lines = generate_s7_source_code_lines(data_from_json) s7_code_lines = generate_s7_source_code_lines(data_from_json)
@ -209,11 +218,11 @@ def main():
for db_index, db_to_document in enumerate(data_from_json["dbs"]): for db_index, db_to_document in enumerate(data_from_json["dbs"]):
if db_index > 0: if db_index > 0:
all_db_markdown_lines.append("\n---\n") # Separador visual entre DBs all_db_markdown_lines.append("\n---\n")
markdown_lines_for_one_db = generate_markdown_table(db_to_document) markdown_lines_for_one_db = generate_markdown_table(db_to_document)
all_db_markdown_lines.extend(markdown_lines_for_one_db) all_db_markdown_lines.extend(markdown_lines_for_one_db)
all_db_markdown_lines.append("") # Espacio después de cada tabla de DB all_db_markdown_lines.append("")
try: try:
with open(md_output_filename, 'w', encoding='utf-8') as f: with open(md_output_filename, 'w', encoding='utf-8') as f:
@ -224,7 +233,6 @@ def main():
print(f"Error al escribir el archivo Markdown {md_output_filename}: {e}") print(f"Error al escribir el archivo Markdown {md_output_filename}: {e}")
else: else:
print(f"No se encontraron DBs en {current_json_filename} para generar documentación Markdown.") print(f"No se encontraron DBs en {current_json_filename} para generar documentación Markdown.")
# Opcionalmente, crear un archivo MD con un mensaje
with open(md_output_filename, 'w', encoding='utf-8') as f: with open(md_output_filename, 'w', encoding='utf-8') as f:
f.write(f"# Documentación S7 para {json_filename_base}\n\n_Fuente JSON: {current_json_filename}_\n\nNo se encontraron Bloques de Datos (DBs) en este archivo JSON.\n") f.write(f"# Documentación S7 para {json_filename_base}\n\n_Fuente JSON: {current_json_filename}_\n\nNo se encontraron Bloques de Datos (DBs) en este archivo JSON.\n")
print(f"Archivo Markdown generado (sin DBs): {md_output_filename}") print(f"Archivo Markdown generado (sin DBs): {md_output_filename}")

View File

@ -1,9 +1,10 @@
# --- x5_refactored.py ---
import json import json
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
import sys import sys
import os import os
import glob # Para buscar archivos JSON import glob
from datetime import datetime # Mover import al inicio from datetime import datetime
script_root = os.path.dirname( script_root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(__file__))) os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
@ -11,6 +12,10 @@ script_root = os.path.dirname(
sys.path.append(script_root) sys.path.append(script_root)
from backend.script_utils import load_configuration from backend.script_utils import load_configuration
# Importar funciones comunes desde x3
from x3 import flatten_db_structure, format_address_for_display
from x4 import format_data_type_for_source
def find_working_directory(): def find_working_directory():
configs = load_configuration() configs = load_configuration()
working_directory = configs.get("working_directory") working_directory = configs.get("working_directory")
@ -19,94 +24,85 @@ def find_working_directory():
sys.exit(1) sys.exit(1)
return working_directory return working_directory
def format_data_type_for_display(var_info: Dict[str, Any]) -> str:
"""Formatea la declaración de tipo para visualización en Markdown."""
base_type = var_info.get("udt_source_name") if var_info.get("udt_source_name") else var_info["data_type"]
type_str = ""
if var_info.get("array_dimensions"):
dims_str = ",".join([f"{d['lower_bound']}..{d['upper_bound']}" for d in var_info["array_dimensions"]])
type_str += f"ARRAY [{dims_str}] OF "
type_str += base_type
if var_info["data_type"].upper() == "STRING" and var_info.get("string_length") is not None:
type_str += f"[{var_info['string_length']}]"
return type_str
def format_offset_for_display(byte_offset: float) -> str:
"""Formatea el offset como X.Y o solo X si es .0."""
if byte_offset == float(int(byte_offset)):
return str(int(byte_offset))
return f"{byte_offset:.1f}"
def generate_members_table_md( def generate_members_table_md(
members: List[Dict[str, Any]], db_info: Dict[str, Any],
path_prefix: str = "",
is_udt_definition: bool = False, is_udt_definition: bool = False,
include_current_value: bool = False include_current_value: bool = True
) -> List[str]: ) -> List[str]:
"""Genera líneas de tabla Markdown para una lista de miembros.""" """
md_lines = [] Genera tabla markdown para todos los miembros usando las funciones de aplanamiento de x3.
for var_info in members: """
name_display = f"{path_prefix}{var_info['name']}" lines = []
# Para miembros expandidos de un UDT, su nombre ya está completo en la jerarquía del JSON. # Definir columnas de la tabla
# La recursión ya habrá construido el path_prefix. if include_current_value:
# No necesitamos hacer nada especial aquí si `is_udt_expanded_member` es true, header = "| Nombre Miembro (Ruta) | Tipo de Dato | Offset (Byte.Bit) | Tamaño (Bytes) | Tamaño (Bits) | Valor Inicial (Decl.) | Valor Actual (Efectivo) | Comentario |"
# ya que esta función se llama recursivamente sobre `children`. separator = "|---|---|---|---|---|---|---|---|"
else:
header = "| Nombre Miembro | Tipo de Dato | Offset (Byte.Bit) | Tamaño (Bytes) | Tamaño (Bits) | Valor Inicial | Comentario |"
separator = "|---|---|---|---|---|---|---|"
data_type_display = format_data_type_for_display(var_info) lines.append(header)
offset_display = format_offset_for_display(var_info['byte_offset']) lines.append(separator)
size_bytes_display = str(var_info['size_in_bytes'])
bit_size_display = str(var_info.get('bit_size', '0')) if var_info.get('bit_size', 0) > 0 else ""
initial_value_display = str(var_info.get('initial_value', '')).replace("|", "\\|").replace("\n", " ") # Usar la función de aplanamiento importada
comment_display = str(var_info.get('comment', '')).replace("|", "\\|").replace("\n", " ") flat_vars = flatten_db_structure(db_info)
row = f"| `{name_display}` | `{data_type_display}` | {offset_display} | {size_bytes_display} | {bit_size_display} | `{initial_value_display}` |" # Generar filas para cada variable
for var in flat_vars:
# Usar la dirección formateada desde flatten_db_structure
address = var.get("address_display", format_address_for_display(var["byte_offset"], var.get("bit_size", 0)))
name_display = f"`{var['full_path']}`"
data_type_display = f"`{format_data_type_for_source(var)}`"
size_bytes_display = str(var.get('size_in_bytes', '0'))
bit_size_display = str(var.get('bit_size', '0')) if var.get('bit_size', 0) > 0 else ""
initial_value = str(var.get('initial_value', '')).replace("|", "\\|").replace("\n", " ")
initial_value_display = f"`{initial_value}`" if initial_value else ""
comment_display = str(var.get('comment', '')).replace("|", "\\|").replace("\n", " ")
if include_current_value: if include_current_value:
current_value_display = "" current_value = str(var.get('current_value', '')).replace("|", "\\|").replace("\n", " ")
# Si es un array y tiene current_element_values current_value_display = f"`{current_value}`" if current_value else ""
if var_info.get("current_element_values") and isinstance(var_info["current_element_values"], dict): row = f"| {name_display} | {data_type_display} | {address} | {size_bytes_display} | {bit_size_display} | {initial_value_display} | {current_value_display} | {comment_display} |"
# Mostrar un resumen o un placeholder para arrays complejos en la tabla principal else:
# Los valores detallados del array se listarán en la sección BEGIN. row = f"| {name_display} | {data_type_display} | {address} | {size_bytes_display} | {bit_size_display} | {initial_value_display} | {comment_display} |"
num_elements = sum(dim['count'] for dim in var_info.get('array_dimensions', [])) if var_info.get('array_dimensions') else 1
if num_elements == 0 and var_info.get("array_dimensions"): # Caso de ARRAY [x..y] donde x > y (raro, pero posible)
num_elements = 1 # Para evitar división por cero o lógica extraña.
assigned_elements = len(var_info["current_element_values"]) lines.append(row)
if assigned_elements > 0:
current_value_display = f"{assigned_elements} elemento(s) asignado(s) en BEGIN"
elif var_info.get("current_value") is not None: # Para arrays con una asignación global (raro en BEGIN)
current_value_display = str(var_info.get("current_value", '')).replace("|", "\\|").replace("\n", " ")
else:
current_value_display = ""
elif var_info.get("current_value") is not None: return lines
current_value_display = str(var_info.get("current_value", '')).replace("|", "\\|").replace("\n", " ")
row += f" `{current_value_display}` |"
row += f" {comment_display} |" def generate_begin_block_documentation(db_info: Dict[str, Any]) -> List[str]:
md_lines.append(row) """
Genera documentación para el bloque BEGIN utilizando flatten_db_structure.
"""
lines = []
lines.append("#### Contenido del Bloque `BEGIN` (Valores Actuales Asignados):")
lines.append("El bloque `BEGIN` define los valores actuales de las variables en el DB. Las siguientes asignaciones son ordenadas por offset:")
lines.append("")
# Recursión para hijos de STRUCTs o miembros expandidos de UDTs # Usar la función de aplanamiento importada de x3
# `is_udt_expanded_member` en el JSON nos dice si los 'children' son la expansión de un UDT. flat_vars = flatten_db_structure(db_info)
if var_info.get("children"):
# El prefijo para los hijos es el nombre completo del padre actual.
# Si el hijo es un miembro expandido de UDT, su propio nombre en 'children' ya es el nombre final del miembro.
# Si el hijo es parte de un STRUCT anidado, su nombre es relativo al STRUCT.
md_lines.extend(generate_members_table_md(
var_info["children"],
f"{name_display}.",
is_udt_definition,
include_current_value
))
return md_lines # Filtrar solo variables con valores actuales
vars_with_values = [var for var in flat_vars if var.get("current_value") is not None]
if vars_with_values:
lines.append("```scl")
for var in vars_with_values:
value_str = str(var["current_value"])
if value_str.lower() == "true": value_str = "TRUE"
elif value_str.lower() == "false": value_str = "FALSE"
lines.append(f" {var['full_path']} := {value_str};")
lines.append("```")
lines.append("")
else:
lines.append("No se encontraron asignaciones de valores actuales.")
lines.append("")
return lines
def generate_json_documentation(data: Dict[str, Any], output_filename: str): def generate_json_documentation(data: Dict[str, Any], output_filename: str):
"""Genera la documentación Markdown completa para el archivo JSON parseado.""" """Genera la documentación Markdown completa para el archivo JSON parseado."""
@ -129,9 +125,10 @@ def generate_json_documentation(data: Dict[str, Any], output_filename: str):
lines.append(f"- **Tamaño Total**: {udt['total_size_in_bytes']} bytes") lines.append(f"- **Tamaño Total**: {udt['total_size_in_bytes']} bytes")
lines.append("") lines.append("")
lines.append("#### Miembros del UDT:") lines.append("#### Miembros del UDT:")
lines.append("| Nombre Miembro | Tipo de Dato | Offset (Byte.Bit) | Tamaño (Bytes) | Tamaño (Bits) | Valor Inicial | Comentario |")
lines.append("|---|---|---|---|---|---|---|") # Usar la función optimizada para generar tabla
lines.extend(generate_members_table_md(udt.get("members", []), is_udt_definition=True, include_current_value=False)) udt_member_lines = generate_members_table_md(udt, is_udt_definition=True, include_current_value=False)
lines.extend(udt_member_lines)
lines.append("") lines.append("")
else: else:
lines.append("No se encontraron UDTs en el archivo JSON.") lines.append("No se encontraron UDTs en el archivo JSON.")
@ -150,28 +147,14 @@ def generate_json_documentation(data: Dict[str, Any], output_filename: str):
lines.append("") lines.append("")
lines.append("#### Miembros del DB (Sección de Declaración):") lines.append("#### Miembros del DB (Sección de Declaración):")
lines.append("| Nombre Miembro (Ruta) | Tipo de Dato | Offset (Byte.Bit) | Tamaño (Bytes) | Tamaño (Bits) | Valor Inicial (Decl.) | Valor Actual (Efectivo) | Comentario |") db_member_lines = generate_members_table_md(db, include_current_value=True)
lines.append("|---|---|---|---|---|---|---|---|") lines.extend(db_member_lines)
lines.extend(generate_members_table_md(db.get("members", []), include_current_value=True))
lines.append("") lines.append("")
# Sección BEGIN # Generar sección BEGIN usando la función optimizada
ordered_assignments = db.get("_begin_block_assignments_ordered") begin_lines = generate_begin_block_documentation(db)
if ordered_assignments: lines.extend(begin_lines)
lines.append("#### Contenido del Bloque `BEGIN` (Valores Actuales Asignados):")
lines.append("El bloque `BEGIN` define los valores actuales de las variables en el DB. Las siguientes asignaciones fueron encontradas, en orden:")
lines.append("")
lines.append("```scl") # Usar SCL para syntax highlighting si el visualizador Markdown lo soporta
for path, value in ordered_assignments:
val_str = str(value)
if val_str.lower() == "true": val_str = "TRUE"
elif val_str.lower() == "false": val_str = "FALSE"
lines.append(f" {path} := {val_str};")
lines.append("```")
lines.append("")
else:
lines.append("No se encontraron asignaciones en el bloque `BEGIN` (o no fue parseado).")
lines.append("")
else: else:
lines.append("No se encontraron DBs en el archivo JSON.") lines.append("No se encontraron DBs en el archivo JSON.")
@ -184,7 +167,6 @@ def generate_json_documentation(data: Dict[str, Any], output_filename: str):
except Exception as e: except Exception as e:
print(f"Error al escribir el archivo Markdown de documentación {output_filename}: {e}") print(f"Error al escribir el archivo Markdown de documentación {output_filename}: {e}")
# --- Main ---
if __name__ == "__main__": if __name__ == "__main__":
working_dir = find_working_directory() working_dir = find_working_directory()
print(f"Using working directory: {working_dir}") print(f"Using working directory: {working_dir}")
@ -199,7 +181,6 @@ if __name__ == "__main__":
if not json_files_to_process: if not json_files_to_process:
print(f"No se encontraron archivos .json en {input_json_dir}") print(f"No se encontraron archivos .json en {input_json_dir}")
else: else:
print(f"Archivos JSON encontrados para procesar: {len(json_files_to_process)}") print(f"Archivos JSON encontrados para procesar: {len(json_files_to_process)}")
for json_input_filepath in json_files_to_process: for json_input_filepath in json_files_to_process:
@ -213,12 +194,6 @@ if __name__ == "__main__":
with open(json_input_filepath, 'r', encoding='utf-8') as f: with open(json_input_filepath, 'r', encoding='utf-8') as f:
data_from_json = json.load(f) data_from_json = json.load(f)
print(f"Archivo JSON '{current_json_filename}' cargado correctamente.") print(f"Archivo JSON '{current_json_filename}' cargado correctamente.")
except FileNotFoundError:
print(f"Error: No se encontró el archivo JSON de entrada: {json_input_filepath}")
continue
except json.JSONDecodeError:
print(f"Error: El archivo JSON de entrada no es válido: {json_input_filepath}")
continue
except Exception as e: except Exception as e:
print(f"Error al leer el archivo JSON {json_input_filepath}: {e}") print(f"Error al leer el archivo JSON {json_input_filepath}: {e}")
continue continue

View File

@ -1,11 +1,11 @@
# --- x6.py --- # --- x6_refactored.py ---
import json import json
from typing import List, Dict, Any from typing import List, Dict, Any
import openpyxl # For Excel export import openpyxl
from openpyxl.utils import get_column_letter from openpyxl.utils import get_column_letter
import sys import sys
import os import os
import glob # Para buscar archivos JSON import glob
script_root = os.path.dirname( script_root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(__file__))) os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
@ -13,6 +13,10 @@ script_root = os.path.dirname(
sys.path.append(script_root) sys.path.append(script_root)
from backend.script_utils import load_configuration from backend.script_utils import load_configuration
# Importar funciones comunes desde x3
from x3 import flatten_db_structure, format_address_for_display
from x4 import format_data_type_for_source
def find_working_directory(): def find_working_directory():
configs = load_configuration() configs = load_configuration()
working_directory = configs.get("working_directory") working_directory = configs.get("working_directory")
@ -21,90 +25,90 @@ def find_working_directory():
sys.exit(1) sys.exit(1)
return working_directory return working_directory
# format_data_type_for_source (copied from x4.py as it's needed)
def format_data_type_for_source(var_info: Dict[str, Any]) -> str:
base_type = var_info.get("udt_source_name") if var_info.get("udt_source_name") else var_info["data_type"]
type_str = ""
if var_info.get("array_dimensions"):
dims_str = ",".join([f"{d['lower_bound']}..{d['upper_bound']}" for d in var_info["array_dimensions"]])
type_str += f"ARRAY [{dims_str}] OF "
type_str += base_type
if var_info["data_type"].upper() == "STRING" and var_info.get("string_length") is not None:
type_str += f"[{var_info['string_length']}]"
return type_str
def generate_excel_table(db_info: Dict[str, Any], excel_filename: str): def generate_excel_table(db_info: Dict[str, Any], excel_filename: str):
""" """
Generates an Excel file with DB documentation. Genera un archivo Excel con documentación del DB usando flatten_db_structure.
""" """
workbook = openpyxl.Workbook() workbook = openpyxl.Workbook()
sheet = workbook.active sheet = workbook.active
db_name_safe = db_info['name'].replace('"', '').replace(' ', '_').replace('/','_') db_name_safe = db_info['name'].replace('"', '').replace(' ', '_').replace('/','_')
sheet.title = f"DB_{db_name_safe}"[:31] # Sheet names have a length limit sheet.title = f"DB_{db_name_safe}"[:31] # Sheet names tienen límite de longitud
headers = ["Address", "Name", "Type", "Initial Value", "Actual Value", "Comment"] # Definir encabezados
headers = ["Address", "Name", "Type", "Size (Bytes)", "Bit Size", "Initial Value", "Actual Value", "Comment"]
for col_num, header in enumerate(headers, 1): for col_num, header in enumerate(headers, 1):
cell = sheet.cell(row=1, column=col_num, value=header) cell = sheet.cell(row=1, column=col_num, value=header)
cell.font = openpyxl.styles.Font(bold=True) cell.font = openpyxl.styles.Font(bold=True)
current_row = 2 # Usar flatten_db_structure importado de x3
processed_expanded_members = set() # To handle expanded UDT members correctly flat_vars = flatten_db_structure(db_info)
def flatten_members_for_excel(members: List[Dict[str, Any]], prefix: str = "", base_offset: float = 0.0, is_expansion: bool = False): # Poblar filas con los datos
nonlocal current_row for row_num, var in enumerate(flat_vars, 2):
for var_idx, var in enumerate(members): # Columna 1: Address
member_id = f"{prefix}{var['name']}_{var_idx}" # Unique ID for processed check address = var.get("address_display", format_address_for_display(var["byte_offset"], var.get("bit_size", 0)))
if is_expansion and member_id in processed_expanded_members: sheet.cell(row=row_num, column=1, value=address)
continue
if is_expansion:
processed_expanded_members.add(member_id)
name_for_display = f"{prefix}{var['name']}" # Columna 2: Name
sheet.cell(row=row_num, column=2, value=var["full_path"])
address = f"{var['byte_offset']:.1f}" if isinstance(var['byte_offset'], float) else str(var['byte_offset']) # Columna 3: Type
# Adjust address formatting for bits as in markdown generation data_type = format_data_type_for_source(var)
if var.get("bit_size", 0) > 0 and isinstance(var['byte_offset'], float) and var['byte_offset'] != int(var['byte_offset']): sheet.cell(row=row_num, column=3, value=data_type)
pass # Already formatted like X.Y
elif var.get("bit_size", 0) > 0 :
address = f"{int(var['byte_offset'])}.0" # Ensure X.0 for bits at the start of a byte
data_type_str = format_data_type_for_source(var) # Columna 4: Size (Bytes)
initial_value = str(var.get("initial_value", "")) sheet.cell(row=row_num, column=4, value=var.get("size_in_bytes", 0))
actual_value = str(var.get("current_value", ""))
comment = str(var.get("comment", ""))
is_struct_container = var["data_type"].upper() == "STRUCT" and \ # Columna 5: Bit Size
not var.get("udt_source_name") and \ sheet.cell(row=row_num, column=5, value=var.get("bit_size", 0) if var.get("bit_size", 0) > 0 else None)
var.get("children")
is_udt_instance_container = bool(var.get("udt_source_name")) and var.get("children")
if not is_struct_container and not is_udt_instance_container or var.get("is_udt_expanded_member"): # Columna 6: Initial Value
row_data = [address, name_for_display, data_type_str, initial_value, actual_value, comment] sheet.cell(row=row_num, column=6, value=var.get("initial_value", ""))
for col_num, value in enumerate(row_data, 1):
sheet.cell(row=current_row, column=col_num, value=value)
current_row += 1
if var.get("children"): # Columna 7: Actual Value
flatten_members_for_excel(var["children"], sheet.cell(row=row_num, column=7, value=var.get("current_value", ""))
f"{name_for_display}.",
var['byte_offset'], # Pass the parent's offset
is_expansion=bool(var.get("udt_source_name"))) # Mark if we are expanding a UDT
flatten_members_for_excel(db_info.get("members", [])) # Columna 8: Comment
sheet.cell(row=row_num, column=8, value=var.get("comment", ""))
# Auto-size columns for better readability # Crear una segunda hoja para el bloque BEGIN
for col_idx, column_cells in enumerate(sheet.columns, 1): begin_sheet = workbook.create_sheet(title="BEGIN_Values")
max_length = 0 begin_headers = ["Address", "Path", "Value"]
column = get_column_letter(col_idx) for col_num, header in enumerate(begin_headers, 1):
for cell in column_cells: cell = begin_sheet.cell(row=1, column=col_num, value=header)
try: cell.font = openpyxl.styles.Font(bold=True)
if len(str(cell.value)) > max_length:
max_length = len(str(cell.value)) # Filtrar solo variables con valores actuales para la hoja BEGIN
except: vars_with_values = [var for var in flat_vars if var.get("current_value") is not None]
pass
adjusted_width = (max_length + 2) # Poblar la hoja BEGIN
sheet.column_dimensions[column].width = adjusted_width for row_num, var in enumerate(vars_with_values, 2):
# Columna 1: Address
begin_sheet.cell(row=row_num, column=1, value=var.get("address_display"))
# Columna 2: Path
begin_sheet.cell(row=row_num, column=2, value=var["full_path"])
# Columna 3: Value
value_str = str(var["current_value"])
if value_str.lower() == "true": value_str = "TRUE"
elif value_str.lower() == "false": value_str = "FALSE"
begin_sheet.cell(row=row_num, column=3, value=value_str)
# Auto-ajustar columnas para mejor legibilidad
for sheet in workbook.worksheets:
for col_idx, column_cells in enumerate(sheet.columns, 1):
max_length = 0
column = get_column_letter(col_idx)
for cell in column_cells:
try:
if len(str(cell.value)) > max_length:
max_length = len(str(cell.value))
except:
pass
adjusted_width = min(max_length + 2, 100) # Limitar a 100 para anchos extremos
sheet.column_dimensions[column].width = adjusted_width
try: try:
workbook.save(excel_filename) workbook.save(excel_filename)
@ -137,20 +141,13 @@ def main():
with open(json_input_filepath, 'r', encoding='utf-8') as f: with open(json_input_filepath, 'r', encoding='utf-8') as f:
data_from_json = json.load(f) data_from_json = json.load(f)
print(f"Archivo JSON '{current_json_filename}' cargado correctamente.") print(f"Archivo JSON '{current_json_filename}' cargado correctamente.")
except FileNotFoundError:
print(f"Error: El archivo JSON de entrada '{current_json_filename}' no fue encontrado en {json_input_filepath}.")
continue
except json.JSONDecodeError:
print(f"Error: El archivo JSON '{current_json_filename}' no tiene un formato JSON válido.")
continue
except Exception as e: except Exception as e:
print(f"Error al cargar/leer {current_json_filename}: {e}") print(f"Error al cargar/leer {current_json_filename}: {e}")
continue continue
if data_from_json.get("dbs"): if data_from_json.get("dbs"):
for db_to_document in data_from_json["dbs"]: for db_to_document in data_from_json["dbs"]:
# Construir el path completo para el archivo Excel de salida excel_output_filename = os.path.join(documentation_dir, f"{current_json_filename}_{db_to_document['name'].replace('"', '')}.xlsx")
excel_output_filename = os.path.join(documentation_dir, f"{current_json_filename}.xlsx")
print(f"Generando documentación Excel para DB: '{db_to_document['name']}' (desde {current_json_filename}) -> {excel_output_filename}") print(f"Generando documentación Excel para DB: '{db_to_document['name']}' (desde {current_json_filename}) -> {excel_output_filename}")
try: try:

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,21 @@
[23:48:43] Iniciando ejecución de x7_value_updater.py en C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001... [02:56:24] Iniciando ejecución de x7_value_updater.py en C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001...
[23:48:43] Using working directory: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001 [02:56:24] Using working directory: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001
[23:48:43] Found _data file: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\db1001_data.db [02:56:24] Los archivos JSON se guardarán en: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json
[23:48:43] Found _format file: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\db1001_format.db [02:56:24] Los archivos de documentación se guardarán en: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation
[23:48:43] Parsing S7 file: db1001_data.db... [02:56:24] Se encontraron 1 pares de archivos para procesar.
[23:48:43] Serializing to JSON: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json\db1001_data_data.json [02:56:24] --- Procesando par de archivos ---
[23:48:43] JSON saved: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json\db1001_data_data.json [02:56:24] Data file: db1001_data.db
[23:48:43] Parsing S7 file: db1001_format.db... [02:56:24] Format file: db1001_format.db
[23:48:43] Serializing to JSON: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json\db1001_format_format.json [02:56:24] Parseando archivo data: db1001_data.db
[23:48:43] JSON saved: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json\db1001_format_format.json [02:56:24] Parseando archivo format: db1001_format.db
[23:48:43] Comparing structure of DB: HMI_Blender_Parameters [02:56:24] Archivos JSON generados: db1001_data.json y db1001_format.json
[23:48:43] La estructura del DB 'HMI_Blender_Parameters' es compatible. [02:56:24] Comparando estructuras para DB 'HMI_Blender_Parameters': 284 variables en _data, 284 variables en _format
[23:48:43] All DB structures are compatible. Proceeding to generate _updated file. [02:56:24] Los archivos son compatibles. Creando el archivo _updated...
[23:48:43] INFO: Usando '_begin_block_assignments_ordered' para generar bloque BEGIN de DB 'HMI_Blender_Parameters'. [02:56:24] Archivo _updated generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\json\db1001_updated.json
[23:48:43] Successfully generated _updated S7 file: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\db1001_updated.db [02:56:25] Archivo de comparación Excel generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_comparison.xlsx
[23:48:43] Ejecución de x7_value_updater.py finalizada (success). Duración: 0:00:00.106052. [02:56:25] Archivo Markdown generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_updated.md
[23:48:43] Log completo guardado en: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\S7_DB_Utils\log_x7_value_updater.txt [02:56:25] Archivo S7 generado: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\documentation\db1001_updated.txt
[02:56:25] Archivo S7 copiado a: C:\Trabajo\SIDEL\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\Reporte\DB1001\db1001_updated.db
[02:56:25] --- Proceso completado ---
[02:56:25] Ejecución de x7_value_updater.py finalizada (success). Duración: 0:00:00.761362.
[02:56:25] Log completo guardado en: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\S7_DB_Utils\log_x7_value_updater.txt