""" convert Markdown tables from adapted IO to Excel for import into TIA Portal Updated to work with paths defined in a shared JSON config file. """ import pandas as pd import openpyxl import re import os import sys import json import tkinter as tk from tkinter import filedialog, messagebox from datetime import datetime # Determine script_root and add to sys.path for custom module import try: current_script_path = os.path.abspath(__file__) script_root = os.path.dirname( os.path.dirname(os.path.dirname(os.path.dirname(current_script_path))) ) if script_root not in sys.path: sys.path.append(script_root) from backend.script_utils import load_configuration except ImportError: print( "Error: No se pudo importar 'load_configuration' desde 'backend.script_utils'." ) sys.exit(1) except NameError: # __file__ is not defined print( "Error: __file__ no está definido. Este script podría no estar ejecutándose en un entorno Python estándar." ) sys.exit(1) def load_path_config(working_directory=None): """ Carga la configuración de paths desde un archivo JSON Si no existe, crea uno con valores predeterminados """ # Si no se proporciona working_directory, usar el directorio actual if not working_directory: try: configs = load_configuration() working_directory = configs.get("working_directory") if not working_directory: print("Error: 'working_directory' no se encontró en la configuración.") working_directory = os.getcwd() except: working_directory = os.getcwd() # Path para el archivo JSON de configuración json_config_path = os.path.join(working_directory, "io_paths_config.json") # Si el archivo existe, cargarlo if os.path.exists(json_config_path): try: with open(json_config_path, 'r', encoding='utf-8') as f: config = json.load(f) print(f"Configuración de paths cargada desde: {json_config_path}") return config except Exception as e: print(f"Error al cargar el archivo de configuración JSON: {e}") return None # Si no existe, crear uno con valores predeterminados default_config = { "paths": [ { "path": "Inputs", "type": "Input", "no_used_path": "IO Not in Hardware\\InputsMaster" }, { "path": "Outputs", "type": "Output", "no_used_path": "IO Not in Hardware\\OutputsMaster" }, { "path": "OutputsFesto", "type": "Output", "no_used_path": "IO Not in Hardware\\OutputsMaster" }, { "path": "IO Not in Hardware\\InputsMaster", "type": "Input", "no_used_path": "IO Not in Hardware\\InputsMaster" }, { "path": "IO Not in Hardware\\OutputsMaster", "type": "Output", "no_used_path": "IO Not in Hardware\\OutputsMaster" } ] } try: with open(json_config_path, 'w', encoding='utf-8') as f: json.dump(default_config, f, indent=2) print(f"Archivo de configuración creado: {json_config_path}") return default_config except Exception as e: print(f"Error al crear el archivo de configuración JSON: {e}") return None def read_markdown_table(file_path): """Leer todas las tablas en formato Markdown que contengan las columnas requeridas y combinarlas en un DataFrame.""" with open(file_path, 'r', encoding='utf-8') as file: content = file.read() # Dividir el contenido en líneas lines = content.strip().split('\n') # Encontrar todas las tablas en el archivo all_tables = [] i = 0 while i < len(lines): # Buscar el inicio de una tabla (línea que comienza con '|') while i < len(lines) and not lines[i].strip().startswith('|'): i += 1 if i >= len(lines): break # Encontrar todas las líneas de esta tabla table_start = i table_lines = [] while i < len(lines): line = lines[i].strip() if line.startswith('|'): table_lines.append(line) i += 1 elif not line: # Línea vacía i += 1 # Verificar si la siguiente línea también es parte de la tabla if i < len(lines) and lines[i].strip().startswith('|'): continue else: break else: break # Si no comienza con '|' y no está vacía, es el final de la tabla if len(table_lines) >= 3: # Necesitamos al menos encabezado, separador y una fila de datos all_tables.append((table_start, table_lines)) print(f"Se encontraron {len(all_tables)} tablas en el archivo") # Procesar cada tabla y verificar si tiene las columnas requeridas valid_dataframes = [] for table_idx, (table_start, table_lines) in enumerate(all_tables): print(f"\nProcesando tabla {table_idx + 1} (línea {table_start + 1})") # Procesar encabezados header_line = table_lines[0] separator_line = table_lines[1] if len(table_lines) > 1 else "" # Verificar que la segunda línea sea realmente un separador is_separator = False if separator_line: is_separator = all(cell.strip().startswith(':') or cell.strip().startswith('-') for cell in separator_line.split('|')[1:-1] if cell.strip()) if not is_separator and len(table_lines) > 1: print(f"Advertencia: La segunda línea no parece ser un separador en tabla {table_idx + 1}. Se asume que es parte de los datos.") separator_idx = None else: separator_idx = 1 # Extraer encabezados header_cells = header_line.split('|') # Eliminar elementos vacíos al principio y al final if not header_cells[0].strip(): header_cells = header_cells[1:] if not header_cells[-1].strip(): header_cells = header_cells[:-1] headers = [h.strip() for h in header_cells] print(f"Encabezados detectados: {headers}") # Verificar si la tabla tiene las columnas requeridas has_io_column = False has_tag_column = False for header in headers: header_lower = header.lower() if header_lower == 'io' or 'address' in header_lower: has_io_column = True if ('master' in header_lower and 'tag' in header_lower) or header_lower == 'master' or header_lower == 'tag': has_tag_column = True if not (has_io_column and has_tag_column): print(f"Tabla {table_idx + 1} no tiene las columnas requeridas (IO/address y Master Tag/master/tag). Omitiendo...") continue print(f"Tabla {table_idx + 1} tiene las columnas requeridas. Procesando...") # Procesar filas de datos data_start_idx = 2 if separator_idx == 1 else 1 data = [] for line in table_lines[data_start_idx:]: # Dividir la línea por el carácter pipe cells = line.split('|') # Eliminar elementos vacíos al principio y al final if not cells[0].strip(): cells = cells[1:] if not cells[-1].strip(): cells = cells[:-1] # Limpiar valores row_values = [cell.strip() for cell in cells] # Asegurar que la fila tenga el mismo número de columnas que los encabezados if len(row_values) != len(headers): print(f"Advertencia: Fila con {len(row_values)} valores, pero se esperaban {len(headers)}. Ajustando...") # Intentar ajustar la fila para que coincida con el número de columnas if len(row_values) < len(headers): row_values.extend([''] * (len(headers) - len(row_values))) else: row_values = row_values[:len(headers)] data.append(row_values) # Convertir a DataFrame if data: # Solo si hay datos df = pd.DataFrame(data, columns=headers) valid_dataframes.append(df) print(f"Tabla {table_idx + 1} procesada exitosamente: {len(df)} filas") else: print(f"Tabla {table_idx + 1} no tiene datos. Omitiendo...") # Combinar todas las tablas válidas if not valid_dataframes: print("No se encontraron tablas válidas con las columnas requeridas") return pd.DataFrame() print(f"\nCombinando {len(valid_dataframes)} tablas válidas...") # Si solo hay un DataFrame, devolverlo directamente if len(valid_dataframes) == 1: combined_df = valid_dataframes[0] else: # Si hay múltiples DataFrames, necesitamos combinarlos # Primero, estandarizar las columnas para que todas tengan los mismos nombres standardized_dfs = [] for df in valid_dataframes: # Crear un DataFrame estandarizado standardized_df = df.copy() # Estandarizar nombres de columnas new_columns = {} for col in df.columns: col_lower = col.lower() if col_lower == 'io' or 'address' in col_lower: new_columns[col] = 'IO' elif 'master' in col_lower and 'tag' in col_lower: new_columns[col] = 'Master Tag' elif col_lower == 'master' or col_lower == 'tag': new_columns[col] = 'Master Tag' standardized_df = standardized_df.rename(columns=new_columns) standardized_dfs.append(standardized_df) # Combinar todos los DataFrames combined_df = pd.concat(standardized_dfs, ignore_index=True) print(f"Tabla combinada final: {len(combined_df)} filas, {len(combined_df.columns)} columnas") print(f"Columnas finales: {list(combined_df.columns)}") return combined_df def create_log_file(output_dir): """Crear un archivo de log con timestamp.""" log_filename = f"update_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt" log_path = os.path.join(output_dir, log_filename) try: with open(log_path, 'w', encoding='utf-8') as log_file: log_file.write(f"Log de actualización de PLCTags - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") log_file.write("=" * 80 + "\n\n") return log_path except Exception as e: print(f"Error al crear el archivo de log: {e}") # Si hay un error, intentar crear en el directorio actual fallback_path = os.path.join(os.getcwd(), log_filename) try: with open(fallback_path, 'w', encoding='utf-8') as log_file: log_file.write(f"Log de actualización de PLCTags - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") log_file.write("=" * 80 + "\n\n") log_file.write(f"ADVERTENCIA: No se pudo crear el log en {output_dir}. Usando directorio alternativo.\n\n") return fallback_path except: print("Error crítico: No se pudo crear ningún archivo de log.") return None def log_message(log_path, message): """Añadir mensaje al log.""" if log_path: try: with open(log_path, 'a', encoding='utf-8') as log_file: log_file.write(message + "\n") except Exception as e: print(f"Error al escribir en el log: {e}") print(message) def get_io_address_format(address, data_type): """ Determinar el formato correcto de dirección IO según el tipo de datos. Args: address: Dirección base (ej: "I0.0", "Q0.0", "PEW0", etc.) data_type: Tipo de datos (Bool, Word, Real, etc.) Returns: str: Dirección formateada según el tipo de datos """ if not address or not isinstance(address, str): return address address = address.strip() data_type_lower = data_type.lower() if data_type else "" # Handle Profibus ranges (extract the first number before the range) profibus_match = re.match(r'^(EW|AW)\s+(\d+)\.\..*$', address) if profibus_match: prefix, number = profibus_match.groups() if prefix == 'EW': if data_type_lower in ['dword', 'udint', 'dint', 'real']: return f"%ED{number}" elif data_type_lower in ['byte', 'usint', 'sint']: return f"%EB{number}" else: return f"%EW{number}" # Word por defecto elif prefix == 'AW': if data_type_lower in ['dword', 'udint', 'dint', 'real']: return f"%AD{number}" elif data_type_lower in ['byte', 'usint', 'sint']: return f"%AB{number}" else: return f"%AW{number}" # Word por defecto # Handle simple AW/EW with space (without range) simple_profibus_match = re.match(r'^(EW|AW)\s+(\d+)$', address) if simple_profibus_match: prefix, number = simple_profibus_match.groups() if prefix == 'EW': if data_type_lower in ['dword', 'udint', 'dint', 'real']: return f"%ED{number}" elif data_type_lower in ['byte', 'usint', 'sint']: return f"%EB{number}" else: return f"%EW{number}" # Word por defecto elif prefix == 'AW': if data_type_lower in ['dword', 'udint', 'dint', 'real']: return f"%AD{number}" elif data_type_lower in ['byte', 'usint', 'sint']: return f"%AB{number}" else: return f"%AW{number}" # Word por defecto # Extraer números de las direcciones para determinar el formato según tipo de datos # Patterns for boolean addresses (mantener formato de bit) if re.match(r'^[IEQ](\d+)\.(\d+)$', address): byte_num = re.match(r'^[IEQ](\d+)\.(\d+)$', address).group(1) bit_num = re.match(r'^[IEQ](\d+)\.(\d+)$', address).group(2) if address.startswith(('I', 'E')): if data_type_lower == 'bool': return f"%E{byte_num}.{bit_num}" elif data_type_lower in ['byte', 'usint', 'sint']: return f"%EB{byte_num}" elif data_type_lower in ['dword', 'udint', 'dint', 'real']: return f"%ED{byte_num}" else: return f"%EW{byte_num}" # Word por defecto elif address.startswith(('Q', 'A')): if data_type_lower == 'bool': return f"%A{byte_num}.{bit_num}" elif data_type_lower in ['byte', 'usint', 'sint']: return f"%AB{byte_num}" elif data_type_lower in ['dword', 'udint', 'dint', 'real']: return f"%AD{byte_num}" else: return f"%AW{byte_num}" # Word por defecto # Patterns for word addresses elif re.match(r'^PEW(\d+)$', address): number = re.match(r'^PEW(\d+)$', address).group(1) if data_type_lower in ['dword', 'udint', 'dint', 'real']: return f"%ED{number}" elif data_type_lower in ['byte', 'usint', 'sint']: return f"%EB{number}" else: return f"%EW{number}" # Word por defecto elif re.match(r'^PAW(\d+)$', address): number = re.match(r'^PAW(\d+)$', address).group(1) if data_type_lower in ['dword', 'udint', 'dint', 'real']: return f"%AD{number}" elif data_type_lower in ['byte', 'usint', 'sint']: return f"%AB{number}" else: return f"%AW{number}" # Word por defecto # Handle addresses that already have % prefix but wrong data type format elif re.match(r'^%[AE][BWDL](\d+)$', address): match = re.match(r'^%([AE])([BWDL])(\d+)$', address) if match: prefix, current_type, number = match.groups() if prefix == 'E': # Input if data_type_lower in ['dword', 'udint', 'dint', 'real']: return f"%ED{number}" elif data_type_lower in ['byte', 'usint', 'sint']: return f"%EB{number}" else: return f"%EW{number}" # Word por defecto elif prefix == 'A': # Output if data_type_lower in ['dword', 'udint', 'dint', 'real']: return f"%AD{number}" elif data_type_lower in ['byte', 'usint', 'sint']: return f"%AB{number}" else: return f"%AW{number}" # Word por defecto # Si ya está en formato correcto o formato desconocido, devolver tal como está return address def transform_io_address(address, data_type=None): """ Transform IO addresses according to the required format, considering data type: For Bool: - Ixx.x → %Exx.x - Qxx.x → %Axx.x For Byte: - Ixx.x → %EBxx - Qxx.x → %ABxx For Word: - Ixx.x → %EWxx - Qxx.x → %AWxx - PEWxx → %EWxx - PAWxx → %AWxx For DWord/Real: - Ixx.x → %EDxx - Qxx.x → %ADxx - PEWxx → %EDxx - PAWxx → %ADxx """ if data_type: return get_io_address_format(address, data_type) # Fallback a la lógica anterior si no se proporciona data_type if not address or not isinstance(address, str): return address address = address.strip() # Handle Profibus ranges (extract the first number before the range) profibus_match = re.match(r'^(EW|AW)\s+(\d+)\.\..*$', address) if profibus_match: prefix, number = profibus_match.groups() if prefix == 'EW': return f"%EW{number}" elif prefix == 'AW': return f"%AW{number}" # Patterns for boolean addresses if re.match(r'^I(\d+)\.(\d+)$', address): return re.sub(r'^I(\d+)\.(\d+)$', r'%E\1.\2', address) elif re.match(r'^E(\d+)\.(\d+)$', address): return re.sub(r'^E(\d+)\.(\d+)$', r'%E\1.\2', address) elif re.match(r'^Q(\d+)\.(\d+)$', address): return re.sub(r'^Q(\d+)\.(\d+)$', r'%A\1.\2', address) elif re.match(r'^A(\d+)\.(\d+)$', address): return re.sub(r'^A(\d+)\.(\d+)$', r'%A\1.\2', address) # Patterns for word addresses elif re.match(r'^PEW(\d+)$', address): return re.sub(r'^PEW(\d+)$', r'%EW\1', address) elif re.match(r'^PAW(\d+)$', address): return re.sub(r'^PAW(\d+)$', r'%AW\1', address) # If already in correct format or unknown format, return as is return address def is_input_tag(tag_name): """Determinar si un tag es de entrada basado en su nombre.""" input_prefixes = ['DI_', 'AI_', 'P_AI_', 'P_FT', 'P_CT', 'P_PT', 'P_TT', 'P_g', 'P_PDS_'] for prefix in input_prefixes: if tag_name.startswith(prefix): # Excepciones para P_g que pueden ser outputs if tag_name.startswith('P_g') and ('_VFC_ControlWord' in tag_name or '_Refvalue' in tag_name): return False return True return False def is_output_tag(tag_name): """Determinar si un tag es de salida basado en su nombre.""" output_prefixes = ['DO_', 'AO_', 'P_AO_', 'P_g', 'MaselliHold', 'MaselliSpare'] for prefix in output_prefixes: if tag_name.startswith(prefix): return True # Si comienza con P_g, revisar si tiene '_VFC_ControlWord' o 'Refvalue' que son outputs if tag_name.startswith('P_g') and ('_VFC_ControlWord' in tag_name or '_VFC_Refvalue' in tag_name): return True # Si comienza con P_PDS_, revisar si son outputs específicos if tag_name.startswith('P_PDS_') and ('_Recipe_Number' in tag_name or '_Freeze_To_PDS' in tag_name or '_Stop_to_PDS' in tag_name): return True return False def is_bit_type(data_type): """Determinar si el tipo de dato es un bit (Bool).""" return data_type.lower() == 'bool' def get_memory_address_format(data_type, mem_byte, mem_bit=None): """ Determinar el formato de dirección de memoria y el incremento de bytes según el tipo de datos. Returns: tuple: (address_format, byte_increment, new_bit) """ data_type_lower = data_type.lower() if data_type_lower == 'bool': # Para Bool usar formato de bit if mem_bit is None: mem_bit = 0 address = f"%M{mem_byte}.{mem_bit}" new_bit = mem_bit + 1 if new_bit > 7: new_bit = 0 byte_increment = 1 else: byte_increment = 0 return address, byte_increment, new_bit elif data_type_lower in ['byte', 'usint', 'sint']: # Para Byte usar %MB (1 byte) address = f"%MB{mem_byte}" return address, 1, None elif data_type_lower in ['word', 'uint', 'int']: # Para Word usar %MW (2 bytes) address = f"%MW{mem_byte}" return address, 2, None elif data_type_lower in ['dword', 'udint', 'dint']: # Para DWord usar %MD (4 bytes) address = f"%MD{mem_byte}" return address, 4, None elif data_type_lower in ['real', 'lreal']: # Para Real usar %MD (4 bytes), para LReal usar %ML (8 bytes) if data_type_lower == 'lreal': address = f"%ML{mem_byte}" return address, 8, None else: address = f"%MD{mem_byte}" return address, 4, None elif data_type_lower in ['time', 'time_of_day', 's5time']: # Para tipos de tiempo usar %MD (4 bytes) address = f"%MD{mem_byte}" return address, 4, None else: # Para tipos desconocidos, usar %MW por defecto (2 bytes) address = f"%MW{mem_byte}" return address, 2, None def update_plc_tags(excel_path, md_path, output_path, log_path): """ Actualiza el archivo Excel con la información del archivo Markdown. Args: excel_path: Ruta al archivo Excel exportado de TIA Portal md_path: Ruta al archivo Markdown con la adaptación IO output_path: Ruta para guardar el Excel actualizado log_path: Ruta para el archivo de log """ log_message(log_path, f"Iniciando proceso de actualización") log_message(log_path, f"Archivo Excel de entrada: {excel_path}") log_message(log_path, f"Archivo Markdown de entrada: {md_path}") log_message(log_path, f"Archivo Excel de salida: {output_path}") log_message(log_path, "-" * 80) # Cargar configuración de paths excel_dir = os.path.dirname(excel_path) path_config = load_path_config(excel_dir) if not path_config: log_message(log_path, "ERROR: No se pudo cargar la configuración de paths") return False # Extraer información de paths desde la configuración path_info = {} for path_entry in path_config["paths"]: path_info[path_entry["path"]] = { "type": path_entry["type"], "no_used_path": path_entry["no_used_path"] } log_message(log_path, f"Configuración de paths cargada:") for path, info in path_info.items(): log_message(log_path, f" - {path}: tipo={info['type']}, no_used_path={info['no_used_path']}") # Leer el archivo Markdown md_df = read_markdown_table(md_path) # Identificar las columnas relevantes en el archivo Markdown io_col = None master_tag_col = None for col in md_df.columns: col_lower = col.lower() if col_lower == 'io' or 'address' in col_lower: io_col = col elif ('master' in col_lower and 'tag' in col_lower) or col_lower == 'master' or col_lower == 'tag': master_tag_col = col if not io_col or not master_tag_col: log_message(log_path, "ERROR: No se pudieron identificar las columnas necesarias en el archivo Markdown") log_message(log_path, f"Columnas disponibles: {list(md_df.columns)}") return False log_message(log_path, f"Columna IO: {io_col}") log_message(log_path, f"Columna Master Tag: {master_tag_col}") # Crear un diccionario de mapeo IO desde el Markdown (sin transformar aún) io_mapping = {} for _, row in md_df.iterrows(): master_tag = str(row[master_tag_col]).strip() io_value = str(row[io_col]).strip() if master_tag and io_value and master_tag != 'nan' and io_value != 'nan': io_mapping[master_tag] = io_value # Guardar la dirección sin transformar log_message(log_path, f"Tags mapeados en el archivo Markdown: {len(io_mapping)}") # Cargar el archivo Excel try: # Usar openpyxl para mantener la estructura del Excel workbook = openpyxl.load_workbook(excel_path) log_message(log_path, f"Archivo Excel cargado: {excel_path}") log_message(log_path, f"Hojas disponibles: {workbook.sheetnames}") except Exception as e: log_message(log_path, f"ERROR: No se pudo cargar el archivo Excel: {e}") return False # Inicializar contadores para direcciones de memoria input_mem_byte = 3600 input_mem_bit = 0 output_mem_byte = 3900 output_mem_bit = 0 # Validar configuración inicial de memoria if output_mem_byte <= input_mem_byte: error_msg = (f"ERROR: Configuración de memoria inválida. " f"output_mem_byte ({output_mem_byte}) debe ser mayor que input_mem_byte ({input_mem_byte}). " f"Ajuste los valores iniciales de memoria.") log_message(log_path, error_msg) return False log_message(log_path, f"Configuración de memoria: input_mem_byte={input_mem_byte}, output_mem_byte={output_mem_byte}") log_message(log_path, f"Espacio disponible para inputs: {output_mem_byte - input_mem_byte} bytes") # Estadísticas total_tags = 0 updated_tags = 0 relocated_tags = {} assigned_memory_addresses = 0 # Inicializar el contador para cada tipo de relocalización for path in path_info.keys(): relocated_tags[path] = 0 # Lista de paths válidos desde la configuración valid_paths = list(path_info.keys()) # Procesamos la hoja principal (asumimos que es la primera) if len(workbook.sheetnames) > 0: sheet = workbook[workbook.sheetnames[0]] # Encontrar las columnas relevantes name_col = None path_col = None data_type_col = None logical_address_col = None for col_idx, cell in enumerate(sheet[1], 1): cell_value = str(cell.value).strip() if cell.value else "" if cell_value.lower() == "name": name_col = col_idx elif cell_value.lower() == "path": path_col = col_idx elif cell_value.lower() == "data type": data_type_col = col_idx elif cell_value.lower() == "logical address": logical_address_col = col_idx if not all([name_col, path_col, data_type_col, logical_address_col]): log_message(log_path, "ERROR: No se encontraron todas las columnas necesarias en el Excel") return False # Convertir a índices base 0 para openpyxl name_col -= 1 path_col -= 1 data_type_col -= 1 logical_address_col -= 1 # Recorrer todas las filas (excluyendo la primera que es el encabezado) for row_idx, row in enumerate(sheet.iter_rows(min_row=2), 2): name_cell = row[name_col] path_cell = row[path_col] data_type_cell = row[data_type_col] logical_address_cell = row[logical_address_col] tag_name = str(name_cell.value).strip() if name_cell.value else "" path = str(path_cell.value).strip() if path_cell.value else "" data_type = str(data_type_cell.value).strip() if data_type_cell.value else "" # Verificar si el path está en la configuración if path in valid_paths: total_tags += 1 # Verificar si el tag está en el mapeo de IO if tag_name in io_mapping: old_address = logical_address_cell.value io_raw_address = io_mapping[tag_name] # Transformar la dirección considerando el tipo de datos new_address = transform_io_address(io_raw_address, data_type) logical_address_cell.value = new_address # Determinar el nuevo path basado en la dirección asignada new_path = path # Por defecto, mantener el mismo path # Determinar el nuevo path según la dirección IO asignada if new_address.startswith("%E"): # Buscar el path configurado para entradas for p, info in path_info.items(): if info.get("type") == "Input" and "Not in Hardware" not in p: new_path = p break elif new_address.startswith("%A"): # Buscar el path configurado para salidas for p, info in path_info.items(): if info.get("type") == "Output" and "Not in Hardware" not in p and p != "OutputsFesto": new_path = p break # Actualizar el path si ha cambiado if new_path != path: path_cell.value = new_path relocated_tags[new_path] = relocated_tags.get(new_path, 0) + 1 updated_tags += 1 log_message(log_path, f"Actualizado: {tag_name} | Tipo: {data_type} | Viejo valor: {old_address} | Nuevo valor: {new_address} | Path: {path_cell.value}") # Si no está en el mapeo, asignar dirección de memoria según configuración else: current_path_info = path_info.get(path, {}) no_used_path = current_path_info.get("no_used_path", "") path_type = current_path_info.get("type", "") # Si tenemos información válida para este path if no_used_path and path_type: # Determinar automáticamente si es entrada o salida is_input = is_input_tag(tag_name) or path_type == "Input" is_output = is_output_tag(tag_name) or path_type == "Output" # Actualizar el path según configuración path_cell.value = no_used_path # Asignar dirección de memoria según el tipo (Input/Output) if path_type == "Input" or (is_input and not is_output): # Verificar que no se exceda el límite de memoria de entrada new_address, byte_increment, new_bit = get_memory_address_format( data_type, input_mem_byte, input_mem_bit ) # Verificar límites antes de asignar new_input_mem_byte = input_mem_byte + byte_increment if new_input_mem_byte >= output_mem_byte: error_msg = (f"ERROR: input_mem_byte ({new_input_mem_byte}) excedería output_mem_byte ({output_mem_byte}). " f"La memoria de entrada está demasiado cerca de la memoria de salida. " f"Tag problemático: {tag_name}. " f"Considere aumentar output_mem_byte o reducir el número de tags de entrada.") log_message(log_path, error_msg) return False if is_bit_type(data_type): input_mem_bit = new_bit input_mem_byte = new_input_mem_byte else: # Tipo Output o no determinado # Asignar dirección de memoria para salidas new_address, byte_increment, new_bit = get_memory_address_format( data_type, output_mem_byte, output_mem_bit ) if is_bit_type(data_type): output_mem_bit = new_bit output_mem_byte += byte_increment relocated_tags[no_used_path] = relocated_tags.get(no_used_path, 0) + 1 old_address = logical_address_cell.value logical_address_cell.value = new_address assigned_memory_addresses += 1 log_message(log_path, f"Asignación memoria: {tag_name} | Tipo: {data_type} | Viejo valor: {old_address} | Nuevo valor: {new_address} | Path: {path_cell.value}") # Guardar el archivo actualizado try: workbook.save(output_path) log_message(log_path, f"Archivo Excel guardado: {output_path}") except Exception as e: log_message(log_path, f"ERROR: No se pudo guardar el archivo Excel: {e}") return False # Mostrar estadísticas log_message(log_path, "\n" + "=" * 30 + " RESUMEN " + "=" * 30) log_message(log_path, f"Total de tags procesados: {total_tags}") log_message(log_path, f"Tags actualizados desde el Markdown: {updated_tags}") for path, count in relocated_tags.items(): if count > 0: log_message(log_path, f"Tags relocalizados a {path}: {count}") log_message(log_path, f"Tags con direcciones de memoria asignadas: {assigned_memory_addresses}") # Mostrar uso final de memoria log_message(log_path, "\n" + "=" * 25 + " USO DE MEMORIA " + "=" * 25) log_message(log_path, f"Memoria de entrada final: {input_mem_byte} (inicio: 3600)") log_message(log_path, f"Memoria de salida final: {output_mem_byte} (inicio: 3900)") log_message(log_path, f"Bytes usados para entradas: {input_mem_byte - 3600}") log_message(log_path, f"Bytes usados para salidas: {output_mem_byte - 3900}") log_message(log_path, f"Espacio restante entre memorias: {3900 - input_mem_byte} bytes") if input_mem_byte > 3600 or output_mem_byte > 3900: log_message(log_path, f"✅ Proceso completado exitosamente sin conflictos de memoria") return True def main(): try: # Intentar cargar la configuración para obtener working_directory from backend.script_utils import load_configuration configs = load_configuration() working_directory = configs.get("working_directory") if not working_directory: working_directory = os.getcwd() except: working_directory = os.getcwd() print(f"Usando directorio de trabajo: {working_directory}") # Crear interfaz para seleccionar archivos root = tk.Tk() root.withdraw() # Ocultar ventana principal # Verificar si existe el archivo PLCTags.xlsx predeterminado en working_directory default_excel_path = os.path.join(working_directory, "PLCTags.xlsx") default_md_path = os.path.join(working_directory, "IO Tags consolidated.md") # Pedir al usuario que seleccione los archivos if os.path.exists(default_excel_path): excel_path = default_excel_path print(f"Usando archivo Excel predeterminado: {excel_path}") else: print("Seleccione el archivo Excel exportado de TIA Portal:") excel_path = filedialog.askopenfilename( title="Seleccione el archivo Excel exportado de TIA Portal", filetypes=[("Excel files", "*.xlsx"), ("All files", "*.*")], initialdir=working_directory ) if not excel_path: print("No se seleccionó ningún archivo Excel. Saliendo...") return if os.path.exists(default_md_path): md_path = default_md_path print(f"Usando archivo Markdown predeterminado: {md_path}") else: print("Seleccione el archivo Markdown con la adaptación IO:") md_path = filedialog.askopenfilename( title="Seleccione el archivo Markdown con la adaptación IO", filetypes=[("Markdown files", "*.md"), ("All files", "*.*")], initialdir=working_directory ) if not md_path: print("No se seleccionó ningún archivo Markdown. Saliendo...") return # Determinar la ruta de salida (mismo directorio que el Excel, pero con "_Updated" añadido) excel_dir = os.path.dirname(excel_path) excel_filename = os.path.basename(excel_path) excel_name, excel_ext = os.path.splitext(excel_filename) output_filename = f"{excel_name}_Updated{excel_ext}" output_path = os.path.join(working_directory, output_filename) # Crear archivo de log log_path = create_log_file(working_directory) # Ejecutar el proceso de actualización success = update_plc_tags(excel_path, md_path, output_path, log_path) if success: messagebox.showinfo("Proceso completado", f"La actualización se ha completado con éxito.\n\n" f"Archivo de salida: {output_path}\n\n" f"Archivo de log: {log_path}") else: messagebox.showerror("Error", f"Hubo un error durante el proceso.\n\n" f"Consulte el archivo de log para más detalles: {log_path}") if __name__ == "__main__": main()