import json from typing import List, Dict, Any, Union # Se asume que las dataclasses (VariableInfo, UdtInfo, etc.) del script x3.py # estarían disponibles si este script fuera parte de un paquete más grande. # Para este script independiente, trabajaremos directamente con los diccionarios del JSON. def format_data_type_for_source(var_info: Dict[str, Any]) -> str: """Formatea la declaración de tipo completa para la variable en S7 source.""" 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_variable_declaration_for_source(var_info: Dict[str, Any], indent_level: int) -> str: """Genera una línea de declaración de variable S7.""" indent_str = " " * indent_level type_declaration_str = format_data_type_for_source(var_info) line = f'{indent_str}{var_info["name"]} : {type_declaration_str}' if var_info.get("initial_value") is not None: # Asegurarse de que los booleanos se escriban como TRUE/FALSE initial_val = var_info["initial_value"] if isinstance(initial_val, bool): initial_val_str = "TRUE" if initial_val else "FALSE" else: initial_val_str = str(initial_val) line += f' := {initial_val_str}' line += ';' if var_info.get("comment"): line += f'\t// {var_info["comment"]}' return line def generate_struct_members_for_source(members: List[Dict[str, Any]], indent_level: int) -> List[str]: """Genera recursivamente las declaraciones de miembros para STRUCTs/UDTs.""" lines = [] for var_info in members: # No expandir UDTs anidados dentro de la sección de declaración de otro UDT o DB. # Solo declarar la variable del tipo UDT. # La expansión de miembros de UDT solo ocurre en el JSON para análisis, no para la reconstrucción de la fuente. if var_info.get("is_udt_expanded_member"): # Estos no se declaran individualmente en el padre. continue if var_info["data_type"].upper() == "STRUCT" and not var_info.get("udt_source_name"): # Es una definición de STRUCT anidada current_indent_str = " " * indent_level lines.append(f'{current_indent_str}{var_info["name"]} : STRUCT;') if var_info.get("children"): lines.extend(generate_struct_members_for_source(var_info["children"], indent_level + 1)) lines.append(f'{current_indent_str}END_STRUCT;') else: # Variable primitiva, String, Array, o instancia de UDT lines.append(generate_variable_declaration_for_source(var_info, indent_level)) return lines def _generate_assignments_recursive(members: List[Dict[str, Any]], path_prefix: str, indent_str: str) -> List[str]: """Ayudante recursivo para generar asignaciones del bloque BEGIN.""" assignment_lines = [] for var_info in members: # Construir la ruta actual para esta variable current_member_name = var_info['name'] current_full_path = f"{path_prefix}{current_member_name}" # Si es una instancia de UDT, sus 'children' en el JSON son los miembros expandidos. # Necesitamos iterar sobre estos 'children' para obtener sus 'current_value'. if var_info.get("udt_source_name") and var_info.get("children"): # Para la instancia de UDT, recursivamente generar asignaciones para sus miembros. # El prefijo de ruta para los miembros del UDT será el nombre de la instancia UDT seguido de un punto. assignment_lines.extend( _generate_assignments_recursive(var_info["children"], f"{current_full_path}.", indent_str) ) # Si es un STRUCT definido inline (no una instancia de UDT) elif var_info["data_type"].upper() == "STRUCT" and not var_info.get("udt_source_name") and var_info.get("children"): assignment_lines.extend( _generate_assignments_recursive(var_info["children"], f"{current_full_path}.", indent_str) ) # Si es un miembro primitivo (o array/string que tiene un current_value directo) # y tiene un 'current_value'. Los miembros expandidos de UDT (is_udt_expanded_member=True) # tendrán su current_value y su current_full_path ya incluirá el nombre de la instancia UDT. elif var_info.get("current_value") is not None: val_str = var_info["current_value"] if isinstance(val_str, bool): # Convertir booleanos de JSON a TRUE/FALSE de S7 val_str = "TRUE" if val_str else "FALSE" assignment_lines.append(f"{indent_str}{current_full_path} := {val_str};") return assignment_lines # En x4.py def generate_begin_block_assignments(db_info: Dict[str, Any], indent_level: int, parsed_json_udts: Dict[str, Dict[str, Any]]) -> List[str]: indent_str = " " * indent_level lines = [] # Utilizar directamente _initial_values_from_begin_block del JSON # ¡ASEGÚRATE DE QUE x3.py INCLUYA ESTE CAMPO EN EL JSON! begin_values_map = db_info.get("_initial_values_from_begin_block") if begin_values_map and isinstance(begin_values_map, dict): # Intentar un ordenamiento simple por clave para una salida más consistente, # aunque el orden original del bloque BEGIN no se garantiza. for path, value_str in sorted(begin_values_map.items()): # Aquí, value_str ya es una cadena. Si necesitáramos convertir booleanos # necesitaríamos información del tipo del 'path', lo cual es complejo aquí. # Asumimos que x3.py guardó los valores en el formato correcto (ej. TRUE/FALSE para bools). # Si x3.py guardó Python bools (true/false), necesitamos convertir. # Para ser seguro, si el valor es "true" o "false" (strings), convertir. # Esta conversión es una heurística. Sería mejor si x3.py ya los formateara. final_value_str = str(value_str) # Asegurar que es string if final_value_str.lower() == "true": final_value_str = "TRUE" elif final_value_str.lower() == "false": final_value_str = "FALSE" lines.append(f"{indent_str}{path} := {final_value_str};") else: # Fallback si _initial_values_from_begin_block no está o está mal formado. # Este fallback ahora necesita ser más inteligente o se eliminará si el principal funciona. # print(f"Advertencia: _initial_values_from_begin_block no encontrado o vacío para DB {db_info['name']}. Reconstrucción de BEGIN puede ser incompleta.") # La función _generate_assignments_recursive anterior podría ser un fallback, # pero depende de que los `current_value` de los elementos de array estén bien poblados. # Si se implementa el `current_element_values` en `VariableInfo` en x3.py: def generate_recursive_fallback(members, prefix, current_indent): fallback_lines = [] for v_info in members: m_name = v_info['name'] m_path = f"{prefix}{m_name}" if v_info.get("current_element_values") and isinstance(v_info["current_element_values"], dict): for index_str, val_str_el in sorted(v_info["current_element_values"].items()): # index_str puede ser "1" o "1,2" etc. el_path = f"{m_path}[{index_str}]" f_val_str_el = str(val_str_el) if f_val_str_el.lower() == "true": f_val_str_el = "TRUE" elif f_val_str_el.lower() == "false": f_val_str_el = "FALSE" fallback_lines.append(f"{current_indent}{el_path} := {f_val_str_el};") elif v_info.get("udt_source_name") and v_info.get("children"): fallback_lines.extend(generate_recursive_fallback(v_info["children"], f"{m_path}.", current_indent)) elif v_info.get("data_type", "").upper() == "STRUCT" and not v_info.get("udt_source_name") and v_info.get("children"): fallback_lines.extend(generate_recursive_fallback(v_info["children"], f"{m_path}.", current_indent)) elif v_info.get("current_value") is not None: val_str_cv = v_info["current_value"] f_val_str_cv = str(val_str_cv) if f_val_str_cv.lower() == "true": f_val_str_cv = "TRUE" elif f_val_str_cv.lower() == "false": f_val_str_cv = "FALSE" fallback_lines.append(f"{current_indent}{m_path} := {f_val_str_cv};") return fallback_lines lines.extend(generate_recursive_fallback(db_info.get("members", []), "", indent_str)) return lines def generate_s7_source_code_lines(data: Dict[str, Any]) -> List[str]: """Genera el código fuente S7 completo (UDTs y DBs) a partir de los datos JSON.""" lines = [] # Generar UDTs for udt in data.get("udts", []): lines.append(f'TYPE "{udt["name"]}"') if udt.get("family"): lines.append(f' FAMILY : {udt["family"]};') if udt.get("version"): lines.append(f' VERSION : {udt["version"]};') lines.append("") # Línea en blanco lines.append(" STRUCT") # Los miembros del UDT están directamente bajo 'members' lines.extend(generate_struct_members_for_source(udt["members"], 2)) # Indentación 2 para miembros lines.append(" END_STRUCT;") lines.append(f'END_TYPE;') lines.append("") # Línea en blanco después de cada UDT # Generar DBs for db in data.get("dbs", []): lines.append(f'DATA_BLOCK "{db["name"]}"') if db.get("title"): lines.append(f' TITLE = {db["title"]};') # Asumir que el título ya tiene el formato correcto if db.get("family"): lines.append(f' FAMILY : {db["family"]};') if db.get("version"): lines.append(f' VERSION : {db["version"]};') lines.append("") # Línea en blanco lines.append(" STRUCT") lines.extend(generate_struct_members_for_source(db["members"], 2)) # Indentación 2 para miembros lines.append(" END_STRUCT;") # Generar bloque BEGIN si hay valores actuales (implicando que hubo un bloque BEGIN) # La forma más fiable es chequear si hay current_values en los miembros. # O, si el parser x3.py guardara una bandera explícita "has_begin_block". # Por ahora, generaremos BEGIN si hay miembros, ya que las asignaciones se basan en current_value. if db.get("members"): # Asumimos que si hay miembros, puede haber un bloque BEGIN. lines.append("BEGIN") lines.extend(generate_begin_block_assignments(db, 1)) # Indentación 1 para asignaciones lines.append(f'END_DATA_BLOCK;') lines.append("") # Línea en blanco después de cada DB return lines def generate_markdown_table(db_info: Dict[str, Any]) -> List[str]: """Genera una tabla Markdown para la documentación de un DB.""" 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("|---|---|---|---|---|---|") def flatten_members_for_markdown(members: List[Dict[str, Any]], prefix: str = "", base_offset: float = 0.0): md_lines = [] for var in members: if var.get("is_udt_expanded_member"): # No listar miembros expandidos como filas separadas de alto nivel aquí continue name = f"{prefix}{var['name']}" # El offset en el JSON ya debería ser absoluto para los miembros del DB. # Para miembros dentro de STRUCTs anidados, el JSON también debería tener offsets absolutos. 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 # El formato X.Y ya está bien para bools elif var.get("bit_size", 0) > 0 : # bool en X.0 address = f"{int(var['byte_offset'])}.0" data_type_str = format_data_type_for_source(var) # Usar la misma función de formato initial_value = str(var.get("initial_value", "")) actual_value = str(var.get("current_value", "")) comment = str(var.get("comment", "")) # Reemplazar pipes en los valores para no romper la tabla Markdown initial_value = initial_value.replace("|", "\\|") actual_value = actual_value.replace("|", "\\|") comment = comment.replace("|", "\\|").replace("\n", " ") md_lines.append(f"| {address} | {name} | {data_type_str} | {initial_value} | {actual_value} | {comment} |") # Si es un STRUCT (no UDT) o un UDT, listar sus miembros constitutivos recursivamente para la documentación if var.get("children"): # Si es una instancia de UDT, los hijos son los miembros expandidos. # Si es un STRUCT, los hijos son los miembros directos del STRUCT. # El prefijo para los hijos debe ser el nombre completo del padre (STRUCT/UDT). md_lines.extend(flatten_members_for_markdown(var["children"], f"{name}.", var['byte_offset'])) return md_lines lines.extend(flatten_members_for_markdown(db_info.get("members", []))) return lines def main(): json_input_filename = "parsed_s7_data_expanded.json" s7_output_filename = "reconstructed_s7_source_v2.txt" # Nuevo nombre para la salida S7 try: with open(json_input_filename, 'r', encoding='utf-8') as f: data_from_json = json.load(f) except FileNotFoundError: print(f"Error: No se encontró el archivo JSON de entrada: {json_input_filename}") return except json.JSONDecodeError: print(f"Error: El archivo JSON de entrada no es válido: {json_input_filename}") return except Exception as e: print(f"Error al leer el archivo JSON {json_input_filename}: {e}") return print(f"Archivo JSON '{json_input_filename}' cargado correctamente.") # 1. Generar el archivo S7 reconstruido s7_code_lines = generate_s7_source_code_lines(data_from_json) try: with open(s7_output_filename, 'w', encoding='utf-8') as f: for line in s7_code_lines: f.write(line + "\n") print(f"Archivo S7 reconstruido generado: {s7_output_filename}") except Exception as e: print(f"Error al escribir el archivo S7 {s7_output_filename}: {e}") # 2. Generar la documentación Markdown (para cada DB encontrado) if data_from_json.get("dbs"): for db_to_document in data_from_json["dbs"]: db_name_safe = db_to_document['name'].replace('"', '').replace(' ', '_') md_filename_specific = f"documentation_db_{db_name_safe}.md" print(f"\nGenerando documentación Markdown para DB: {db_to_document['name']}...") markdown_lines = generate_markdown_table(db_to_document) try: with open(md_filename_specific, 'w', encoding='utf-8') as f: for line in markdown_lines: f.write(line + "\n") print(f"Archivo Markdown de documentación generado: {md_filename_specific}") except Exception as e: print(f"Error al escribir el archivo Markdown {md_filename_specific}: {e}") else: print("No se encontraron DBs en el archivo JSON para generar documentación.") if __name__ == "__main__": main()