ParamManagerScripts/backend/script_groups/S7_DB_Utils/x5.py

232 lines
11 KiB
Python

import json
from typing import List, Dict, Any, Optional
import sys
import os
import glob # Para buscar archivos JSON
from datetime import datetime # Mover import al inicio
script_root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
)
sys.path.append(script_root)
from backend.script_utils import load_configuration
def find_working_directory():
configs = load_configuration()
working_directory = configs.get("working_directory")
if not working_directory:
print("No working directory specified in the configuration file.")
sys.exit(1)
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(
members: List[Dict[str, Any]],
path_prefix: str = "",
is_udt_definition: bool = False,
include_current_value: bool = False
) -> List[str]:
"""Genera líneas de tabla Markdown para una lista de miembros."""
md_lines = []
for var_info in members:
name_display = f"{path_prefix}{var_info['name']}"
# Para miembros expandidos de un UDT, su nombre ya está completo en la jerarquía del JSON.
# La recursión ya habrá construido el path_prefix.
# No necesitamos hacer nada especial aquí si `is_udt_expanded_member` es true,
# ya que esta función se llama recursivamente sobre `children`.
data_type_display = format_data_type_for_display(var_info)
offset_display = format_offset_for_display(var_info['byte_offset'])
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", " ")
comment_display = str(var_info.get('comment', '')).replace("|", "\\|").replace("\n", " ")
row = f"| `{name_display}` | `{data_type_display}` | {offset_display} | {size_bytes_display} | {bit_size_display} | `{initial_value_display}` |"
if include_current_value:
current_value_display = ""
# Si es un array y tiene current_element_values
if var_info.get("current_element_values") and isinstance(var_info["current_element_values"], dict):
# Mostrar un resumen o un placeholder para arrays complejos en la tabla principal
# Los valores detallados del array se listarán en la sección BEGIN.
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"])
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:
current_value_display = str(var_info.get("current_value", '')).replace("|", "\\|").replace("\n", " ")
row += f" `{current_value_display}` |"
row += f" {comment_display} |"
md_lines.append(row)
# Recursión para hijos de STRUCTs o miembros expandidos de UDTs
# `is_udt_expanded_member` en el JSON nos dice si los 'children' son la expansión de un UDT.
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
def generate_json_documentation(data: Dict[str, Any], output_filename: str):
"""Genera la documentación Markdown completa para el archivo JSON parseado."""
lines = []
lines.append(f"# Documentación del Archivo de Datos S7 Parseado")
current_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
lines.append(f"_Generado el: {current_date}_")
lines.append("")
lines.append("Este documento describe la estructura y el contenido del archivo JSON generado por el parser de fuentes S7 (`x3.py`).")
lines.append("")
# --- Sección UDTs ---
lines.append("## 1. Tipos de Datos de Usuario (UDTs)")
lines.append("")
if data.get("udts"):
for udt in data["udts"]:
lines.append(f'### 1.{data["udts"].index(udt) + 1}. UDT: `{udt["name"]}`')
if udt.get("family"): lines.append(f"- **Familia**: {udt['family']}")
if udt.get("version"): lines.append(f"- **Versión**: {udt['version']}")
lines.append(f"- **Tamaño Total**: {udt['total_size_in_bytes']} bytes")
lines.append("")
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("|---|---|---|---|---|---|---|")
lines.extend(generate_members_table_md(udt.get("members", []), is_udt_definition=True, include_current_value=False))
lines.append("")
else:
lines.append("No se encontraron UDTs en el archivo JSON.")
lines.append("")
# --- Sección DBs ---
lines.append("## 2. Bloques de Datos (DBs)")
lines.append("")
if data.get("dbs"):
for db in data["dbs"]:
lines.append(f'### 2.{data["dbs"].index(db) + 1}. DB: `{db["name"]}`')
if db.get("title"): lines.append(f"- **TITLE**: `{db['title']}`")
if db.get("family"): lines.append(f"- **Familia**: {db['family']}")
if db.get("version"): lines.append(f"- **Versión**: {db['version']}")
lines.append(f"- **Tamaño Declaraciones**: {db['total_size_in_bytes']} bytes")
lines.append("")
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 |")
lines.append("|---|---|---|---|---|---|---|---|")
lines.extend(generate_members_table_md(db.get("members", []), include_current_value=True))
lines.append("")
# Sección BEGIN
ordered_assignments = db.get("_begin_block_assignments_ordered")
if ordered_assignments:
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:
lines.append("No se encontraron DBs en el archivo JSON.")
# Guardar el archivo Markdown
try:
with open(output_filename, 'w', encoding='utf-8') as f:
for line in lines:
f.write(line + "\n")
print(f"Documentación Markdown completa generada: {output_filename}")
except Exception as e:
print(f"Error al escribir el archivo Markdown de documentación {output_filename}: {e}")
# --- Main ---
if __name__ == "__main__":
working_dir = find_working_directory()
print(f"Using working directory: {working_dir}")
input_json_dir = os.path.join(working_dir, "json")
documentation_dir = os.path.join(working_dir, "documentation")
os.makedirs(documentation_dir, exist_ok=True)
print(f"Los archivos Markdown de descripción se guardarán en: {documentation_dir}")
json_files_to_process = glob.glob(os.path.join(input_json_dir, "*.json"))
if not json_files_to_process:
print(f"No se encontraron archivos .json en {input_json_dir}")
else:
print(f"Archivos JSON encontrados para procesar: {len(json_files_to_process)}")
for json_input_filepath in json_files_to_process:
json_filename_base = os.path.splitext(os.path.basename(json_input_filepath))[0]
current_json_filename = os.path.basename(json_input_filepath)
print(f"\n--- Procesando archivo JSON para descripción: {current_json_filename} ---")
markdown_output_filename = os.path.join(documentation_dir, f"{json_filename_base}_description.md")
try:
with open(json_input_filepath, 'r', encoding='utf-8') as f:
data_from_json = json.load(f)
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:
print(f"Error al leer el archivo JSON {json_input_filepath}: {e}")
continue
try:
generate_json_documentation(data_from_json, markdown_output_filename)
except Exception as e:
print(f"Error al generar la documentación para {current_json_filename}: {e}")
print("\n--- Proceso de generación de descripciones Markdown completado ---")