# --- x7_refactored.py --- import json import os import glob import sys import copy import shutil # Para copiar archivos from typing import Dict, List, Tuple, Any, Optional # Importar para el path 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 # Importar desde x3 from x3 import S7Parser, find_working_directory, custom_json_serializer, flatten_db_structure, format_address_for_display, access_by_hierarchy_path from x4 import format_data_type_for_source # Importar desde x4 para generar archivos from x4 import generate_s7_source_code_lines, generate_markdown_table def find_matching_files(working_dir: str) -> List[Tuple[str, str]]: """ Busca pares de archivos _data y _format con extensión .db o .awl. """ # [Código existente] data_files_db = glob.glob(os.path.join(working_dir, "*_data.db")) data_files_awl = glob.glob(os.path.join(working_dir, "*_data.awl")) all_data_files = data_files_db + data_files_awl format_files_db = glob.glob(os.path.join(working_dir, "*_format.db")) format_files_awl = glob.glob(os.path.join(working_dir, "*_format.awl")) all_format_files = format_files_db + format_files_awl matched_pairs = [] for data_file in all_data_files: base_name = os.path.basename(data_file).replace("_data", "").split('.')[0] format_candidates = [f for f in all_format_files if os.path.basename(f).startswith(f"{base_name}_format")] if format_candidates: matched_pairs.append((data_file, format_candidates[0])) return matched_pairs # [Otras funciones existentes: parse_files_to_json, compare_structures_by_offset, update_values_recursive, create_updated_json] def parse_files_to_json(data_file: str, format_file: str, json_dir: str) -> Tuple[Dict, Dict]: """ Parsea los archivos _data y _format usando S7Parser y guarda los resultados como JSON. """ data_parser = S7Parser() format_parser = S7Parser() print(f"Parseando archivo data: {os.path.basename(data_file)}") data_result = data_parser.parse_file(data_file) print(f"Parseando archivo format: {os.path.basename(format_file)}") format_result = format_parser.parse_file(format_file) data_base = os.path.splitext(os.path.basename(data_file))[0] format_base = os.path.splitext(os.path.basename(format_file))[0] data_json_path = os.path.join(json_dir, f"{data_base}.json") format_json_path = os.path.join(json_dir, f"{format_base}.json") data_json = json.dumps(data_result, default=custom_json_serializer, indent=2) format_json = json.dumps(format_result, default=custom_json_serializer, indent=2) with open(data_json_path, "w", encoding='utf-8') as f: f.write(data_json) with open(format_json_path, "w", encoding='utf-8') as f: f.write(format_json) print(f"Archivos JSON generados: {os.path.basename(data_json_path)} y {os.path.basename(format_json_path)}") data_obj = json.loads(data_json) format_obj = json.loads(format_json) return data_obj, format_obj def compare_structures_by_offset(data_vars: List[Dict], format_vars: List[Dict]) -> Tuple[bool, List[str]]: """ Compara variables por offset, verificando compatibilidad. Usa las listas aplanadas de flatten_db_structure. """ issues = [] # Crear diccionarios para búsqueda rápida por offset data_by_offset = {var["byte_offset"]: var for var in data_vars} format_by_offset = {var["byte_offset"]: var for var in format_vars} # Recopilar todos los offsets únicos de ambos conjuntos all_offsets = sorted(set(list(data_by_offset.keys()) + list(format_by_offset.keys()))) # Verificar que todos los offsets existan en ambos conjuntos for offset in all_offsets: if offset not in data_by_offset: issues.append(f"Offset {offset} existe en _format pero no en _data") continue if offset not in format_by_offset: issues.append(f"Offset {offset} existe en _data pero no en _format") continue # Obtener las variables para comparar data_var = data_by_offset[offset] format_var = format_by_offset[offset] # Verificar coincidencia de tipos data_type = data_var["data_type"].upper() format_type = format_var["data_type"].upper() if data_type != format_type: issues.append(f"Tipo de dato diferente en offset {offset}: {data_type} ({data_var['full_path']}) vs {format_type} ({format_var['full_path']})") # Verificar tamaño data_size = data_var["size_in_bytes"] format_size = format_var["size_in_bytes"] if data_size != format_size: issues.append(f"Tamaño diferente en offset {offset}: {data_size} bytes ({data_var['full_path']}) vs {format_size} bytes ({format_var['full_path']})") # Verificar tamaño en bits para BOOLs data_bit_size = data_var.get("bit_size", 0) format_bit_size = format_var.get("bit_size", 0) if data_bit_size != format_bit_size: issues.append(f"Tamaño en bits diferente en offset {offset}: {data_bit_size} ({data_var['full_path']}) vs {format_bit_size} ({format_var['full_path']})") return len(issues) == 0, issues def process_updated_json(updated_json: Dict, updated_json_path: str, working_dir: str, documentation_dir: str, original_format_file: str): """ Genera los archivos markdown y S7 a partir del JSON actualizado, y copia el archivo S7 al directorio de trabajo con la extensión correcta. """ # Obtener nombre base y extensión original format_file_name = os.path.basename(original_format_file) base_name = format_file_name.replace("_format", "_updated").split('.')[0] original_extension = os.path.splitext(format_file_name)[1] # .db o .awl # Generar archivo markdown para documentación for db in updated_json.get("dbs", []): md_output_filename = os.path.join(documentation_dir, f"{base_name}.md") try: md_lines = [] md_lines.append(f"# Documentación S7 para {base_name}") md_lines.append(f"_Fuente JSON: {os.path.basename(updated_json_path)}_") md_lines.append("") # Generar tabla markdown usando generate_markdown_table importado de x4 db_md_lines = generate_markdown_table(db) md_lines.extend(db_md_lines) with open(md_output_filename, 'w', encoding='utf-8') as f: for line in md_lines: f.write(line + "\n") print(f"Archivo Markdown generado: {md_output_filename}") except Exception as e: print(f"Error al generar Markdown para {base_name}: {e}") # Generar archivo de código fuente S7 s7_txt_filename = os.path.join(documentation_dir, f"{base_name}.txt") try: s7_lines = generate_s7_source_code_lines(updated_json) with open(s7_txt_filename, 'w', encoding='utf-8') as f: for line in s7_lines: f.write(line + "\n") print(f"Archivo S7 generado: {s7_txt_filename}") # Copiar al directorio de trabajo con la extensión original s7_output_filename = os.path.join(working_dir, f"{base_name}{original_extension}") shutil.copy2(s7_txt_filename, s7_output_filename) print(f"Archivo S7 copiado a: {s7_output_filename}") except Exception as e: print(f"Error al generar archivo S7 para {base_name}: {e}") def create_updated_json(data_json: Dict, format_json: Dict) -> Dict: """ Crea un JSON actualizado basado en la estructura de _format con valores de _data. Usa punteros jerárquicos para acceso directo a variables, evitando errores de tipo. Args: data_json: JSON con los datos fuente (_data) format_json: JSON con la estructura y nombres (_format) Returns: JSON actualizado con estructura de format_json y valores de data_json """ # Copia profunda para no modificar el original updated_json = copy.deepcopy(format_json) # Procesar cada DB for db_idx, format_db in enumerate(format_json.get("dbs", [])): # Buscar el DB correspondiente en data_json data_db = next((db for db in data_json.get("dbs", []) if db["name"] == format_db["name"]), None) if not data_db: print(f"Error: No se encontró DB '{format_db['name']}' en data_json") continue # Aplanar variables de ambos DBs flat_data_vars = flatten_db_structure(data_db) flat_format_vars = flatten_db_structure(format_db) print(f"Procesando DB '{format_db['name']}': {len(flat_format_vars)} variables en _format, {len(flat_data_vars)} variables en _data") # Crear mapa de offset a variable para data - solo incluir tipos de valor data_by_offset = { var["byte_offset"]: var for var in flat_data_vars if var.get("element_type") in ["SIMPLE_VAR", "ARRAY_ELEMENT"] } # Contar variables actualizadas para estadísticas variables_updated = 0 variables_missing = 0 # Para cada variable en format, buscar su correspondiente en data por offset for format_var in flat_format_vars: # Solo procesar tipos de valor (variables y elementos de array) if format_var.get("element_type") not in ["SIMPLE_VAR", "ARRAY_ELEMENT"]: continue offset = format_var["byte_offset"] path = format_var["full_path"] # Encontrar la variable correspondiente en data_json por offset if offset in data_by_offset: data_var = data_by_offset[offset] # Verificar compatibilidad de tipos format_element_type = format_var.get("element_type") data_element_type = data_var.get("element_type") if format_element_type != data_element_type and not ( format_element_type in ["SIMPLE_VAR", "ARRAY_ELEMENT"] and data_element_type in ["SIMPLE_VAR", "ARRAY_ELEMENT"] ): print(f"Advertencia: Tipos no compatibles en offset {offset}: {format_element_type} vs {data_element_type}") variables_missing += 1 continue # Usar el puntero jerárquico para acceso directo hierarchy_path = format_var.get("_hierarchy_path") if not hierarchy_path: print(f"Error: No se encontró ruta jerárquica para {path}") variables_missing += 1 continue # Caso especial para elementos de array if format_var.get("is_array_element") and "_array_index" in format_var: # Obtener el array padre array_var = access_by_hierarchy_path(updated_json["dbs"][db_idx], hierarchy_path) if array_var and array_var.get("element_type") == "ARRAY": # Asegurar que current_element_values existe if "current_element_values" not in array_var: array_var["current_element_values"] = {} # Copiar el valor del elemento array_index = format_var["_array_index"] if "current_value" in data_var: array_var["current_element_values"][array_index] = { "value": data_var["current_value"], "offset": data_var["byte_offset"] } variables_updated += 1 else: print(f"Error: El nodo padre para el elemento de array {path} no es un array válido") variables_missing += 1 else: # Para variables normales, acceder directamente target_var = access_by_hierarchy_path(updated_json["dbs"][db_idx], hierarchy_path) if target_var and target_var.get("element_type") in ["SIMPLE_VAR", "ARRAY"]: # Copiar initial_value si existe if "initial_value" in target_var: del target_var["initial_value"] if "initial_value" in data_var and data_var["initial_value"] is not None: target_var["initial_value"] = data_var["initial_value"] # Copiar current_value si existe if "current_value" in target_var: del target_var["current_value"] if "current_value" in data_var and data_var["current_value"] is not None: target_var["current_value"] = data_var["current_value"] # Para variables tipo ARRAY, también copiar current_element_values if target_var.get("element_type") == "ARRAY" and "current_element_values" in data_var: if "current_element_values" in target_var: del target_var["current_element_values"] if data_var["current_element_values"]: target_var["current_element_values"] = copy.deepcopy(data_var["current_element_values"]) variables_updated += 1 else: type_info = f" (tipo: {target_var.get('element_type')})" if target_var else "" print(f"Error: No se pudo acceder o actualizar la variable {path}{type_info}") variables_missing += 1 else: # Offset no encontrado en data_json, reportar error print(f"Error: Offset {offset} (para '{path}') no encontrado en los datos source (_data)") variables_missing += 1 print(f"Estadísticas para DB '{format_db['name']}': {variables_updated} variables actualizadas, {variables_missing} no encontradas") return updated_json def generate_comparison_excel(format_json: Dict, data_json: Dict, updated_json: Dict, excel_filename: str): """ Genera un archivo Excel comparando valores entre format, data y updated JSONs. Usa flatten_db_structure con punteros jerárquicos y filtra por tipos de elemento. Args: format_json: JSON con la estructura y nombres de formato data_json: JSON con los datos source updated_json: JSON con los datos actualizados excel_filename: Ruta del archivo Excel a generar """ import openpyxl from openpyxl.utils import get_column_letter from openpyxl.styles import PatternFill, Font, Alignment, Border, Side # Crear un nuevo libro de Excel workbook = openpyxl.Workbook() sheet = workbook.active sheet.title = "Value_Comparison" # Definir estilos diff_fill = PatternFill(start_color="FFFF00", end_color="FFFF00", fill_type="solid") type_mismatch_fill = PatternFill(start_color="FF9999", end_color="FF9999", fill_type="solid") # Light red header_font = Font(bold=True) header_fill = PatternFill(start_color="DDDDDD", end_color="DDDDDD", fill_type="solid") thin_border = Border(left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), bottom=Side(style='thin')) # Configurar encabezados headers = ["Address", "Name", "Type", "Element Type", "Format Initial", "Data Initial", "Updated Initial", "Format Current", "Data Current", "Updated Current", "Type Match", "Value Differences"] for col_num, header in enumerate(headers, 1): cell = sheet.cell(row=1, column=col_num, value=header) cell.font = header_font cell.fill = header_fill cell.border = thin_border cell.alignment = Alignment(horizontal='center') # Congelar primera fila sheet.freeze_panes = "A2" current_row = 2 # Procesar cada DB for db_idx, format_db in enumerate(format_json.get("dbs", [])): db_name = format_db["name"] data_db = next((db for db in data_json.get("dbs", []) if db["name"] == db_name), None) updated_db = next((db for db in updated_json.get("dbs", []) if db["name"] == db_name), None) if not data_db or not updated_db: print(f"Error: DB '{db_name}' no encontrado en alguno de los archivos JSON") continue # Añadir título con el nombre del DB sheet.merge_cells(f'A{current_row}:L{current_row}') header_cell = sheet.cell(row=current_row, column=1, value=f"DB: {db_name}") header_cell.font = Font(bold=True, size=12) header_cell.fill = PatternFill(start_color="CCCCFF", end_color="CCCCFF", fill_type="solid") # Light blue header_cell.alignment = Alignment(horizontal='center') current_row += 1 # Obtener variables aplanadas de todas las fuentes flat_format_vars = flatten_db_structure(format_db) flat_data_vars = flatten_db_structure(data_db) flat_updated_vars = flatten_db_structure(updated_db) # Crear mapas por offset para búsqueda rápida - FILTRAR por element_type data_by_offset = { var["byte_offset"]: var for var in flat_data_vars if var.get("element_type") in ["SIMPLE_VAR", "ARRAY_ELEMENT"] } updated_by_offset = { var["byte_offset"]: var for var in flat_updated_vars if var.get("element_type") in ["SIMPLE_VAR", "ARRAY_ELEMENT"] } # Procesar cada variable desde format_json for format_var in flat_format_vars: # Omitir tipos que no son valores (STRUCT, UDT_INSTANCE) element_type = format_var.get("element_type", "UNKNOWN") if element_type not in ["SIMPLE_VAR", "ARRAY_ELEMENT"]: continue offset = format_var["byte_offset"] path = format_var["full_path"] data_type = format_data_type_for_source(format_var) address = format_var.get("address_display", format_address_for_display(offset, format_var.get("bit_size", 0))) # Encontrar variables correspondientes por offset data_var = data_by_offset.get(offset) updated_var = updated_by_offset.get(offset) # Comparar tipos de elemento data_element_type = data_var.get("element_type", "MISSING") if data_var else "MISSING" updated_element_type = updated_var.get("element_type", "MISSING") if updated_var else "MISSING" # Determinar compatibilidad de tipos type_match = "Yes" if data_var and element_type != data_element_type: # Verificar tipos compatibles if (element_type in ["SIMPLE_VAR", "ARRAY_ELEMENT"] and data_element_type in ["SIMPLE_VAR", "ARRAY_ELEMENT"]): type_match = "Compatible" else: type_match = "No" elif not data_var: type_match = "Missing" # Obtener valores (con cadenas vacías por defecto) format_initial = str(format_var.get("initial_value", "")) data_initial = str(data_var.get("initial_value", "")) if data_var else "" updated_initial = str(updated_var.get("initial_value", "")) if updated_var else "" format_current = str(format_var.get("current_value", "")) data_current = str(data_var.get("current_value", "")) if data_var else "" updated_current = str(updated_var.get("current_value", "")) if updated_var else "" # Verificar diferencias has_initial_diff = (format_initial != data_initial or format_initial != updated_initial or data_initial != updated_initial) has_current_diff = (format_current != data_current or format_current != updated_current or data_current != updated_current) # Crear descripción detallada de diferencias diff_desc = [] if has_initial_diff: diff_desc.append("Initial values differ") if has_current_diff: diff_desc.append("Current values differ") if not diff_desc: diff_desc.append("None") # Escribir datos sheet.cell(row=current_row, column=1, value=address) sheet.cell(row=current_row, column=2, value=path) sheet.cell(row=current_row, column=3, value=data_type) sheet.cell(row=current_row, column=4, value=element_type) sheet.cell(row=current_row, column=5, value=format_initial) sheet.cell(row=current_row, column=6, value=data_initial) sheet.cell(row=current_row, column=7, value=updated_initial) sheet.cell(row=current_row, column=8, value=format_current) sheet.cell(row=current_row, column=9, value=data_current) sheet.cell(row=current_row, column=10, value=updated_current) sheet.cell(row=current_row, column=11, value=type_match) sheet.cell(row=current_row, column=12, value=", ".join(diff_desc)) # Añadir bordes a todas las celdas for col in range(1, 13): sheet.cell(row=current_row, column=col).border = thin_border # Resaltar diferencias if has_initial_diff: for col in range(5, 8): sheet.cell(row=current_row, column=col).fill = diff_fill if has_current_diff: for col in range(8, 11): sheet.cell(row=current_row, column=col).fill = diff_fill # Resaltar incompatibilidades de tipo if type_match == "No" or type_match == "Missing": sheet.cell(row=current_row, column=11).fill = type_mismatch_fill current_row += 1 # Añadir filtro a los encabezados sheet.auto_filter.ref = f"A1:L{current_row-1}" # Auto-ajustar anchos de columna for col_idx, column_cells in enumerate(sheet.columns, 1): max_length = 3 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, 20) # Limitar ancho máximo sheet.column_dimensions[column].width = adjusted_width # Añadir hoja de resumen summary_sheet = workbook.create_sheet(title="Summary") summary_sheet.column_dimensions['A'].width = 30 summary_sheet.column_dimensions['B'].width = 15 summary_sheet.column_dimensions['C'].width = 50 # Añadir encabezado al resumen summary_headers = ["Database", "Value Variables", "Notes"] for col_num, header in enumerate(summary_headers, 1): cell = summary_sheet.cell(row=1, column=col_num, value=header) cell.font = header_font cell.fill = header_fill # Añadir datos de resumen summary_row = 2 for db_idx, format_db in enumerate(format_json.get("dbs", [])): db_name = format_db["name"] data_db = next((db for db in data_json.get("dbs", []) if db["name"] == db_name), None) updated_db = next((db for db in updated_json.get("dbs", []) if db["name"] == db_name), None) if not data_db or not updated_db: continue flat_format_vars = flatten_db_structure(format_db) flat_data_vars = flatten_db_structure(data_db) # Filtrar solo variables de valor (SIMPLE_VAR y ARRAY_ELEMENT) format_value_vars = [var for var in flat_format_vars if var.get("element_type") in ["SIMPLE_VAR", "ARRAY_ELEMENT"]] # Contar por tipo de elemento format_type_counts = {} for var in flat_format_vars: element_type = var.get("element_type", "UNKNOWN") format_type_counts[element_type] = format_type_counts.get(element_type, 0) + 1 # Crear mapa filtrado de offset de datos data_by_offset = { var["byte_offset"]: var for var in flat_data_vars if var.get("element_type") in ["SIMPLE_VAR", "ARRAY_ELEMENT"] } # Contar diferencias de valor y desajustes de tipo diff_count = 0 type_mismatch_count = 0 missing_count = 0 for format_var in format_value_vars: offset = format_var["byte_offset"] data_var = data_by_offset.get(offset) if data_var: # Verificar desajuste de tipo if format_var.get("element_type") != data_var.get("element_type"): type_mismatch_count += 1 # Verificar diferencias de valor format_initial = str(format_var.get("initial_value", "")) data_initial = str(data_var.get("initial_value", "")) format_current = str(format_var.get("current_value", "")) data_current = str(data_var.get("current_value", "")) if format_initial != data_initial or format_current != data_current: diff_count += 1 else: missing_count += 1 # Escribir en el resumen summary_sheet.cell(row=summary_row, column=1, value=db_name) summary_sheet.cell(row=summary_row, column=2, value=len(format_value_vars)) notes = [] for element_type, count in format_type_counts.items(): notes.append(f"{element_type}: {count}") notes.append(f"Value differences: {diff_count}") notes.append(f"Type mismatches: {type_mismatch_count}") notes.append(f"Missing in data: {missing_count}") summary_sheet.cell(row=summary_row, column=3, value=", ".join(notes)) summary_row += 1 try: workbook.save(excel_filename) print(f"Archivo Excel de comparación generado: {excel_filename}") except Exception as e: print(f"Error al escribir el archivo Excel {excel_filename}: {e}") def main(): working_dir = find_working_directory() print(f"Using working directory: {working_dir}") output_json_dir = os.path.join(working_dir, "json") documentation_dir = os.path.join(working_dir, "documentation") os.makedirs(output_json_dir, exist_ok=True) os.makedirs(documentation_dir, exist_ok=True) print(f"Los archivos JSON se guardarán en: {output_json_dir}") print(f"Los archivos de documentación se guardarán en: {documentation_dir}") matched_pairs = find_matching_files(working_dir) if not matched_pairs: print("No se encontraron pares de archivos _data y _format para procesar.") return print(f"Se encontraron {len(matched_pairs)} pares de archivos para procesar.") for data_file, format_file in matched_pairs: print(f"\n--- Procesando par de archivos ---") print(f"Data file: {os.path.basename(data_file)}") print(f"Format file: {os.path.basename(format_file)}") # Parsear archivos a JSON data_json, format_json = parse_files_to_json(data_file, format_file, output_json_dir) # Verificar compatibilidad usando listas aplanadas all_compatible = True for db_idx, format_db in enumerate(format_json.get("dbs", [])): # Buscar el DB correspondiente en data_json data_db = next((db for db in data_json.get("dbs", []) if db["name"] == format_db["name"]), None) if not data_db: print(f"Error: No se encontró DB '{format_db['name']}' en el archivo data") all_compatible = False continue # Aplanar variables de ambos DBs flat_data_vars = flatten_db_structure(data_db) flat_format_vars = flatten_db_structure(format_db) print(f"Comparando estructuras para DB '{format_db['name']}': {len(flat_data_vars)} variables en _data, {len(flat_format_vars)} variables en _format") compatible, issues = compare_structures_by_offset(flat_data_vars, flat_format_vars) if not compatible: all_compatible = False print(f"\nSe encontraron problemas de compatibilidad en DB '{format_db['name']}':") for issue in issues: print(f" - {issue}") print(f"Abortando el proceso para este DB.") if all_compatible: print("\nLos archivos son compatibles. Creando el archivo _updated...") # Crear JSON actualizado updated_json = create_updated_json(data_json, format_json) # Guardar la versión actualizada base_name = os.path.basename(format_file).replace("_format", "").split('.')[0] updated_json_path = os.path.join(output_json_dir, f"{base_name}_updated.json") with open(updated_json_path, "w", encoding='utf-8') as f: json.dump(updated_json, f, default=custom_json_serializer, indent=2) print(f"Archivo _updated generado: {updated_json_path}") # Generar archivo de comparación Excel comparison_excel_path = os.path.join(documentation_dir, f"{base_name}_comparison.xlsx") generate_comparison_excel(format_json, data_json, updated_json, comparison_excel_path) # Procesar el JSON actualizado para generar archivos Markdown y S7 process_updated_json(updated_json, updated_json_path, working_dir, documentation_dir, format_file) print("\n--- Proceso completado ---") if __name__ == "__main__": main()