From 38c54ab40b0e631d318897ac01a00dbc33cec91b Mon Sep 17 00:00:00 2001 From: Miguel Date: Mon, 24 Mar 2025 20:14:15 +0100 Subject: [PATCH] Agregado de progress dialog --- Parsers/S7ProjectParser.cs | 658 ++++++++++++++++++++++-------------- Services/S7ParserService.cs | 258 ++++++++++++++ ViewModels/MainViewModel.cs | 169 ++++++++- Views/MainWindow.xaml | 66 +++- Views/MainWindow.xaml.cs | 21 +- 5 files changed, 897 insertions(+), 275 deletions(-) create mode 100644 Services/S7ParserService.cs diff --git a/Parsers/S7ProjectParser.cs b/Parsers/S7ProjectParser.cs index 062af74..674ade7 100644 --- a/Parsers/S7ProjectParser.cs +++ b/Parsers/S7ProjectParser.cs @@ -3,7 +3,10 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; +using System.Globalization; +using System.Xml; using S7Explorer.Models; namespace S7Explorer.Parsers @@ -13,6 +16,12 @@ namespace S7Explorer.Parsers private readonly string _projectFilePath; private readonly string _projectDirectory; + // Evento para notificar mensajes de log + public event EventHandler LogEvent; + + // Evento para notificar actualizaciones del árbol + public event EventHandler StructureUpdatedEvent; + public S7ProjectParser(string projectFilePath) { _projectFilePath = projectFilePath; @@ -22,64 +31,23 @@ namespace S7Explorer.Parsers throw new ArgumentException("No se pudo determinar el directorio del proyecto"); } - public async Task ParseProjectAsync() - { - return await Task.Run(() => - { - // Crear objeto de proyecto - var project = new S7Project(_projectFilePath); - - try - { - // Estructura básica del proyecto - var devicesFolder = new S7Object - { - Name = "Dispositivos", - ObjectType = S7ObjectType.Folder, - Parent = project - }; - project.Children.Add(devicesFolder); - - // Parsear dispositivos - ParseDevices(devicesFolder); - - // Para uso en el futuro: más carpetas de alto nivel - var sharedFolder = new S7Object - { - Name = "Datos Globales", - ObjectType = S7ObjectType.Folder, - Parent = project - }; - project.Children.Add(sharedFolder); - - return project; - } - catch (Exception ex) - { - // En caso de error, al menos retornamos un proyecto básico con el error - var errorObject = new S7Object - { - Name = "Error al parsear proyecto", - Description = ex.Message, - ObjectType = S7ObjectType.Folder, - Parent = project - }; - project.Children.Add(errorObject); - - return project; - } - }); - } - - private void ParseDevices(S7Object devicesFolder) + public void ParseDevices(S7Object devicesFolder, CancellationToken cancellationToken = default) { try { - // Obtener lista de dispositivos a partir del archivo de información + Log("Iniciando parseo de dispositivos"); + + // Obtener lista de dispositivos a partir de los archivos de información var deviceIdInfos = ParseDeviceIdInfos(); + Log($"Se encontraron {deviceIdInfos.Count} dispositivos"); foreach (var deviceInfo in deviceIdInfos) { + // Verificar cancelación + cancellationToken.ThrowIfCancellationRequested(); + + Log($"Procesando dispositivo: {deviceInfo.Name}"); + var device = new S7Object { Name = deviceInfo.Name, @@ -88,28 +56,74 @@ namespace S7Explorer.Parsers }; devicesFolder.Children.Add(device); + // Notificar actualización de UI + NotifyStructureUpdated(devicesFolder); + // Crear carpetas para cada tipo de bloque var dbFolder = CreateBlockFolder(device, "Bloques de datos (DB)"); var fbFolder = CreateBlockFolder(device, "Bloques de función (FB)"); var fcFolder = CreateBlockFolder(device, "Funciones (FC)"); var obFolder = CreateBlockFolder(device, "Bloques de organización (OB)"); + var udtFolder = CreateBlockFolder(device, "Tipos de datos (UDT)"); var symbolsFolder = CreateBlockFolder(device, "Tabla de símbolos"); - // Parsear bloques y símbolos si se dispone de IDs + // Notificar actualización de UI nuevamente + NotifyStructureUpdated(device); + + // Parsear símbolos - esto lo hacemos primero porque suele ser más rápido if (deviceInfo.SymbolListId.HasValue) { - ParseSymbols(symbolsFolder, deviceInfo.SymbolListId.Value); + Log($"Parseando símbolos con ID: 0x{deviceInfo.SymbolListId.Value:X8}"); + ParseSymbolsList(symbolsFolder, deviceInfo.SymbolListId.Value, cancellationToken); + // Notificar actualización de UI + NotifyStructureUpdated(symbolsFolder); + } + else + { + LogWarning("No se encontró ID de lista de símbolos para este dispositivo"); } + // Parsear bloques if (deviceInfo.SubblockListId.HasValue) { - ParseBlocks(dbFolder, fbFolder, fcFolder, obFolder, deviceInfo.SubblockListId.Value); + Log($"Parseando bloques con ID: 0x{deviceInfo.SubblockListId.Value:X8}"); + + // Parseamos por tipo y notificamos después de cada tipo para no bloquear la UI + ParseBlocksOfType(dbFolder, "00006", "DB", deviceInfo.SubblockListId.Value, cancellationToken); + NotifyStructureUpdated(dbFolder); + + cancellationToken.ThrowIfCancellationRequested(); + ParseBlocksOfType(fbFolder, "00004", "FB", deviceInfo.SubblockListId.Value, cancellationToken); + NotifyStructureUpdated(fbFolder); + + cancellationToken.ThrowIfCancellationRequested(); + ParseBlocksOfType(fcFolder, "00003", "FC", deviceInfo.SubblockListId.Value, cancellationToken); + NotifyStructureUpdated(fcFolder); + + cancellationToken.ThrowIfCancellationRequested(); + ParseBlocksOfType(obFolder, "00008", "OB", deviceInfo.SubblockListId.Value, cancellationToken); + NotifyStructureUpdated(obFolder); + + cancellationToken.ThrowIfCancellationRequested(); + ParseBlocksOfType(udtFolder, "00001", "UDT", deviceInfo.SubblockListId.Value, cancellationToken); + NotifyStructureUpdated(udtFolder); + } + else + { + LogWarning("No se encontró ID de lista de bloques para este dispositivo"); } } } + catch (OperationCanceledException) + { + LogWarning("Parseo de dispositivos cancelado por el usuario"); + throw; + } catch (Exception ex) { // Si falla el parseo, añadimos un nodo de error pero seguimos con el resto + LogError($"Error al parsear dispositivos: {ex.Message}"); + var errorNode = new S7Object { Name = "Error al parsear dispositivos", @@ -118,6 +132,7 @@ namespace S7Explorer.Parsers Parent = devicesFolder }; devicesFolder.Children.Add(errorNode); + NotifyStructureUpdated(devicesFolder); } } @@ -137,16 +152,33 @@ namespace S7Explorer.Parsers { var result = new List(); - // Esto es una simplificación - en una implementación real - // necesitarías parsear los archivos reales como en el proyecto C++ - var s7resoffPath = Path.Combine(_projectDirectory, "hrs", "S7RESOFF.DBF"); - - if (File.Exists(s7resoffPath)) + try { - try + // Parsear S7RESOFF.DBF + var s7resoffPath = Path.Combine(_projectDirectory, "hrs", "S7RESOFF.DBF"); + if (File.Exists(s7resoffPath)) { - // Leer la tabla de dispositivos + Log($"Leyendo archivo S7RESOFF.DBF: {s7resoffPath}"); var records = DbfParser.ReadDbfFile(s7resoffPath, new[] { "ID", "NAME", "RSRVD4_L" }); + Log($"Se encontraron {records.Count} registros en S7RESOFF.DBF"); + + // Leer linkhrs.lnk para obtener IDs de Subblock y SymbolList + var linkhrsPath = Path.Combine(_projectDirectory, "hrs", "linkhrs.lnk"); + byte[] linkhrsData = null; + if (File.Exists(linkhrsPath)) + { + Log($"Leyendo archivo linkhrs.lnk: {linkhrsPath}"); + linkhrsData = File.ReadAllBytes(linkhrsPath); + Log($"Leídos {linkhrsData.Length} bytes de linkhrs.lnk"); + } + else + { + LogWarning($"Archivo linkhrs.lnk no encontrado: {linkhrsPath}"); + } + + // Constantes para IDs + const uint SubblockListIdMagic = 0x00116001; + const uint SymbolListIdMagic = 0x00113001; foreach (var record in records) { @@ -155,258 +187,378 @@ namespace S7Explorer.Parsers Name = DbfParser.ConvertCP1252ToUtf8(record["NAME"]), }; - // Procesamiento simplificado para IDs de Subblock y Symbol List - device.SubblockListId = DbfParser.StringToInt(record["RSRVD4_L"]); + // Obtener offset en linkhrs.lnk + var offset = DbfParser.StringToInt(record["RSRVD4_L"]); + if (offset.HasValue && linkhrsData != null && offset.Value < linkhrsData.Length - 512) + { + Log($"Procesando dispositivo '{device.Name}' con offset: {offset.Value}"); - // NOTA: En una implementación completa, tendrías que leer linkhrs.lnk - // para obtener SubblockListId y SymbolListId reales + // Leer 512 bytes (128 uint32) desde el offset + for (int i = offset.Value; i < offset.Value + 512 - 8; i += 4) + { + if (i + 4 >= linkhrsData.Length) break; + + uint value = BitConverter.ToUInt32(linkhrsData, i); + if (value == SubblockListIdMagic && i + 4 < linkhrsData.Length) + { + device.SubblockListId = BitConverter.ToUInt32(linkhrsData, i + 4); + Log($" Encontrado SubblockListId: 0x{device.SubblockListId:X8}"); + } + else if (value == SymbolListIdMagic && i + 4 < linkhrsData.Length) + { + device.SymbolListId = BitConverter.ToUInt32(linkhrsData, i + 4); + Log($" Encontrado SymbolListId: 0x{device.SymbolListId:X8}"); + } + } + } + else + { + LogWarning($"No se pudo obtener offset para '{device.Name}' o offset inválido"); + } result.Add(device); } } - catch (Exception) + else { - // Error de parseo - ignorar y continuar + LogWarning($"Archivo S7RESOFF.DBF no encontrado: {s7resoffPath}"); } } + catch (Exception ex) + { + // Registrar error pero continuar + LogError($"Error parseando información de dispositivos: {ex.Message}"); + } - // Si no se encuentran dispositivos, crear uno de ejemplo + // Si no encontramos dispositivos, crear uno simple con valores por defecto if (result.Count == 0) { + LogWarning("No se encontraron dispositivos, creando dispositivo por defecto"); result.Add(new DeviceIdInfo { - Name = "Dispositivo de ejemplo" + Name = Path.GetFileNameWithoutExtension(_projectFilePath), + SubblockListId = 0, + SymbolListId = 0 }); } return result; } - private void ParseSymbols(S7Object symbolsFolder, int symbolListId) - { - // Esta es una implementación de muestra - // En una versión completa, leerías los archivos YDBs reales - - // Crear algunos símbolos de ejemplo - var symbols = new List - { - new S7Symbol { - Name = "Motor_Start", - Address = "I0.0", - DataType = "BOOL", - Comment = "Pulsador de inicio del motor" - }, - new S7Symbol { - Name = "Motor_Stop", - Address = "I0.1", - DataType = "BOOL", - Comment = "Pulsador de parada del motor" - }, - new S7Symbol { - Name = "Motor_Running", - Address = "Q0.0", - DataType = "BOOL", - Comment = "Motor en marcha" - }, - new S7Symbol { - Name = "Temperature", - Address = "IW64", - DataType = "INT", - Comment = "Temperatura del proceso" - }, - }; - - foreach (var symbol in symbols) - { - symbol.Parent = symbolsFolder; - symbolsFolder.Children.Add(symbol); - } - } - - private void ParseBlocks(S7Object dbFolder, S7Object fbFolder, S7Object fcFolder, S7Object obFolder, int subblockListId) + private void ParseSymbolsList(S7Object symbolsFolder, uint symbolListId, CancellationToken cancellationToken = default) { try { - // Usar parser específico para las funciones (FC) - var fcParser = new FCParser(_projectDirectory); - var functions = fcParser.ParseFunctionBlocks(subblockListId); + // Verificar cancelación + cancellationToken.ThrowIfCancellationRequested(); - // Añadir bloques FC al árbol de proyecto - foreach (var fc in functions) + // Construir la ruta del archivo de símbolos basada en SymbolListId + string symbolsPath = FindSymbolsPath(symbolListId); + if (string.IsNullOrEmpty(symbolsPath)) { - fc.Parent = fcFolder; - fcFolder.Children.Add(fc); + LogWarning($"No se pudo encontrar el archivo de símbolos para ID: 0x{symbolListId:X8}"); + return; } - // Para los otros tipos de bloques, seguimos usando los ejemplos por ahora - // En una implementación completa, crearíamos parsers específicos para cada tipo - AddSampleDataBlocks(dbFolder); - AddSampleFunctionBlocks(fbFolder); - AddSampleOrgBlocks(obFolder); + Log($"Procesando archivo de símbolos: {symbolsPath}"); + + // Leer SYMLIST.DBF + var records = DbfParser.ReadDbfFile(symbolsPath, new[] { "_SKZ", "_OPIEC", "_DATATYP", "_COMMENT" }); + Log($"Se encontraron {records.Count} registros en SYMLIST.DBF"); + + int symbolCount = 0; + int updateFrequency = Math.Max(1, records.Count / 10); // Actualizar UI cada 10% aprox + + foreach (var record in records) + { + // Verificar cancelación periódicamente + if (symbolCount % 50 == 0) + { + cancellationToken.ThrowIfCancellationRequested(); + } + + // Solo procesar entradas con código + if (!string.IsNullOrEmpty(record["_OPIEC"])) + { + string code = record["_OPIEC"].Trim(); + + // Solo queremos símbolos I, M, Q y DB para la lista de símbolos + if (code.StartsWith("I") || code.StartsWith("M") || code.StartsWith("Q") || code.StartsWith("DB")) + { + var symbol = new S7Symbol + { + Name = DbfParser.ConvertCP1252ToUtf8(record["_SKZ"]), + Address = code, + DataType = record["_DATATYP"], + Comment = DbfParser.ConvertCP1252ToUtf8(record["_COMMENT"]), + Parent = symbolsFolder, + ObjectType = S7ObjectType.Symbol + }; + symbolsFolder.Children.Add(symbol); + symbolCount++; + + // Actualizar UI periódicamente para mostrar progreso + if (symbolCount % updateFrequency == 0) + { + Log($"Procesados {symbolCount} símbolos..."); + NotifyStructureUpdated(symbolsFolder); + } + } + } + } + + Log($"Se agregaron {symbolCount} símbolos a la tabla de símbolos"); + } + catch (OperationCanceledException) + { + LogWarning("Parseo de símbolos cancelado por el usuario"); + throw; } catch (Exception ex) { - // En caso de error, añadimos un objeto de error informativo - var errorObject = new S7Object + LogError($"Error parseando símbolos: {ex.Message}"); + + var errorSymbol = new S7Symbol { - Name = "Error en parseo de bloques", + Name = "Error al parsear símbolos", Description = ex.Message, - ObjectType = S7ObjectType.Folder + Parent = symbolsFolder, + ObjectType = S7ObjectType.Symbol }; + symbolsFolder.Children.Add(errorSymbol); + NotifyStructureUpdated(symbolsFolder); + } + } - // Lo añadimos a cada carpeta para que sea visible - dbFolder.Children.Add(errorObject); - fbFolder.Children.Add(new S7Object + private string FindSymbolsPath(uint symbolListId) + { + try + { + // Buscar en SYMLISTS.DBF el path basado en el ID + var symlistsPath = Path.Combine(_projectDirectory, "YDBs", "SYMLISTS.DBF"); + if (File.Exists(symlistsPath)) { - Name = errorObject.Name, - Description = errorObject.Description, - ObjectType = errorObject.ObjectType - }); - fcFolder.Children.Add(new S7Object + Log($"Buscando ruta de símbolos en SYMLISTS.DBF para ID: 0x{symbolListId:X8}"); + var records = DbfParser.ReadDbfFile(symlistsPath, new[] { "_ID", "_DBPATH" }); + + foreach (var record in records) + { + var id = DbfParser.StringToInt(record["_ID"]); + if (id.HasValue && id.Value == symbolListId) + { + string dbPath = record["_DBPATH"]; + string fullPath = Path.Combine(_projectDirectory, "YDBs", dbPath, "SYMLIST.DBF"); + Log($"Encontrada ruta de símbolos: {fullPath}"); + return fullPath; + } + } + + LogWarning($"No se encontró registro en SYMLISTS.DBF para ID: 0x{symbolListId:X8}"); + } + else { - Name = errorObject.Name, - Description = errorObject.Description, - ObjectType = errorObject.ObjectType - }); - obFolder.Children.Add(new S7Object + LogWarning($"Archivo SYMLISTS.DBF no encontrado: {symlistsPath}"); + } + } + catch (Exception ex) + { + LogError($"Error buscando ruta de símbolos: {ex.Message}"); + } + + // Si no se encuentra, intentar una búsqueda directa + string symbolFolderPath = Path.Combine(_projectDirectory, "YDBs", symbolListId.ToString("X8")); + string symbolFilePath = Path.Combine(symbolFolderPath, "SYMLIST.DBF"); + if (Directory.Exists(symbolFolderPath) && File.Exists(symbolFilePath)) + { + Log($"Encontrada ruta de símbolos mediante búsqueda directa: {symbolFilePath}"); + return symbolFilePath; + } + + return null; + } + + private void ParseBlocksOfType(S7Object blockFolder, string blockType, string prefix, + uint subblockListId, CancellationToken cancellationToken = default) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + + // Construir la ruta a SUBBLK.DBF + string subblockPath = Path.Combine(_projectDirectory, "ombstx", "offline", + subblockListId.ToString("X8"), "SUBBLK.DBF"); + + if (!File.Exists(subblockPath)) { - Name = errorObject.Name, - Description = errorObject.Description, - ObjectType = errorObject.ObjectType + LogWarning($"Archivo SUBBLK.DBF no encontrado: {subblockPath}"); + return; + } + + Log($"Procesando bloques de tipo {prefix} desde: {subblockPath}"); + + // Leer SUBBLK.DBF + var records = DbfParser.ReadDbfFile(subblockPath, new[] + { + "SUBBLKTYP", "BLKNUMBER", "BLKNAME", "AUTHOR", + "FAMILY", "VERSION", "CREATEDATE", "MODDATE" }); - } - } - private void AddSampleDataBlocks(S7Object dbFolder) - { - var dbs = new List - { - new S7DataBlock { - Name = "Datos_proceso", - Number = "DB1", - Size = 124, - IsInstanceDb = false, - Description = "Datos de proceso", - Modified = DateTime.Now.AddDays(-5) - }, - new S7DataBlock { - Name = "Parámetros", - Number = "DB2", - Size = 234, - IsInstanceDb = false, - Description = "Parámetros de configuración", - Modified = DateTime.Now.AddDays(-10) - }, - new S7DataBlock { - Name = "Motor_Inst", - Number = "DB10", - Size = 86, - IsInstanceDb = true, - Description = "Instancia de FB1", - Modified = DateTime.Now.AddDays(-2) + // Filtrar registros para este tipo de bloque + var blockRecords = records.Where(r => r["SUBBLKTYP"] == blockType).ToList(); + Log($"Se encontraron {blockRecords.Count} bloques {prefix}"); + + if (blockRecords.Count == 0) + { + return; } - }; - foreach (var db in dbs) - { - db.Parent = dbFolder; - dbFolder.Children.Add(db); - } - } + // Contador de bloques + int blockCount = 0; + int updateFrequency = Math.Max(1, blockRecords.Count / 5); // Actualizar UI cada 20% aprox - private void AddSampleFunctionBlocks(S7Object fbFolder) - { - var fbs = new List - { - new S7FunctionBlock { - Name = "Motor_Control", - Number = "FB1", - Size = 328, - Language = "SCL", - Description = "Control de motor", - Modified = DateTime.Now.AddDays(-15) - }, - new S7FunctionBlock { - Name = "PID_Control", - Number = "FB2", - Size = 512, - Language = "SCL", - Description = "Controlador PID", - Modified = DateTime.Now.AddDays(-20) + // Definir tipo de objeto según prefijo + S7ObjectType objectType; + switch (prefix) + { + case "DB": objectType = S7ObjectType.DataBlock; break; + case "FB": objectType = S7ObjectType.FunctionBlock; break; + case "FC": objectType = S7ObjectType.Function; break; + case "OB": objectType = S7ObjectType.Organization; break; + default: objectType = S7ObjectType.Folder; break; } - }; - foreach (var fb in fbs) - { - fb.Parent = fbFolder; - fbFolder.Children.Add(fb); - } - } + // Procesar cada registro + foreach (var record in blockRecords) + { + // Verificar cancelación periódicamente + if (blockCount % 20 == 0) + { + cancellationToken.ThrowIfCancellationRequested(); + } - private void AddSampleFunctions(S7Object fcFolder) - { - var fcs = new List - { - new S7Function { - Name = "Calc_Setpoint", - Number = "FC1", - Size = 124, - ReturnType = "REAL", - Description = "Cálculo de punto de consigna", - Modified = DateTime.Now.AddDays(-8) - }, - new S7Function { - Name = "Scale_Analog", - Number = "FC2", - Size = 68, - ReturnType = "REAL", - Description = "Escalado de valor analógico", - Modified = DateTime.Now.AddDays(-12) + // Solo procesar si tenemos un número de bloque válido + if (int.TryParse(record["BLKNUMBER"], out int blockNumber)) + { + // Crear objeto de bloque + var block = new S7Block + { + Name = DbfParser.ConvertCP1252ToUtf8(record["BLKNAME"]), + Number = $"{prefix}{blockNumber}", + AuthorName = DbfParser.ConvertCP1252ToUtf8(record["AUTHOR"]), + Family = DbfParser.ConvertCP1252ToUtf8(record["FAMILY"]), + Version = record["VERSION"], + ObjectType = objectType, + Parent = blockFolder + }; + + // Intentar extraer fecha de modificación + if (DateTime.TryParse(record["MODDATE"], out DateTime modDate)) + { + block.Modified = modDate; + } + + // Añadir a la carpeta correspondiente + blockFolder.Children.Add(block); + blockCount++; + + // Log detallado cada cierto número de bloques para evitar saturar el log + if (blockCount <= 5 || blockCount % 20 == 0) + { + Log($"Agregado bloque {block.Number} - {block.Name}"); + } + + // Actualizar UI periódicamente para mostrar progreso + if (blockCount % updateFrequency == 0) + { + Log($"Procesados {blockCount}/{blockRecords.Count} bloques {prefix}..."); + NotifyStructureUpdated(blockFolder); + } + } } - }; - foreach (var fc in fcs) + // Ordenar bloques por número + SortBlocksInFolder(blockFolder); + + Log($"Completado procesamiento de {blockCount} bloques {prefix}"); + } + catch (OperationCanceledException) { - fc.Parent = fcFolder; - fcFolder.Children.Add(fc); + LogWarning($"Parseo de bloques {prefix} cancelado por el usuario"); + throw; + } + catch (Exception ex) + { + LogError($"Error en parseo de bloques {prefix}: {ex.Message}"); + blockFolder.Children.Add(new S7Object + { + Name = $"Error en bloques {prefix}", + Description = ex.Message, + Parent = blockFolder + }); + NotifyStructureUpdated(blockFolder); } } - private void AddSampleOrgBlocks(S7Object obFolder) + private void SortBlocksInFolder(S7Object folder) { - var obs = new List - { - new S7Block { - Name = "Main", - Number = "OB1", - Size = 256, - ObjectType = S7ObjectType.Organization, - Description = "Ciclo principal", - Modified = DateTime.Now.AddDays(-1) - }, - new S7Block { - Name = "Clock_100ms", - Number = "OB35", - Size = 124, - ObjectType = S7ObjectType.Organization, - Description = "Interrupción cíclica 100ms", - Modified = DateTime.Now.AddDays(-7) - } - }; + // Crear una nueva lista ordenada + var sortedList = folder.Children + .OrderBy(block => { + if (block.Number == null) return int.MaxValue; - foreach (var ob in obs) + string numPart = new string(block.Number + .SkipWhile(c => !char.IsDigit(c)) + .TakeWhile(char.IsDigit) + .ToArray()); + + return int.TryParse(numPart, out int num) ? num : int.MaxValue; + }) + .ToList(); + + // Limpiar y añadir los elementos ordenados + folder.Children.Clear(); + foreach (var item in sortedList) { - ob.Parent = obFolder; - obFolder.Children.Add(ob); + folder.Children.Add(item); } } + private void NotifyStructureUpdated(S7Object obj) + { + StructureUpdatedEvent?.Invoke(this, obj); + } + + #region Logging Methods + + private void Log(string message) + { + string timestamp = DateTime.Now.ToString("HH:mm:ss.fff"); + string logMessage = $"[{timestamp}] {message}"; + LogEvent?.Invoke(this, logMessage); + } + + private void LogWarning(string message) + { + string timestamp = DateTime.Now.ToString("HH:mm:ss.fff"); + string logMessage = $"[{timestamp}] ADVERTENCIA: {message}"; + LogEvent?.Invoke(this, logMessage); + } + + private void LogError(string message) + { + string timestamp = DateTime.Now.ToString("HH:mm:ss.fff"); + string logMessage = $"[{timestamp}] ERROR: {message}"; + LogEvent?.Invoke(this, logMessage); + } + + #endregion + // Clase interna para la información de los dispositivos private class DeviceIdInfo { public string Name { get; set; } - public int? SubblockListId { get; set; } - public int? SymbolListId { get; set; } + public uint? SubblockListId { get; set; } + public uint? SymbolListId { get; set; } } } } \ No newline at end of file diff --git a/Services/S7ParserService.cs b/Services/S7ParserService.cs new file mode 100644 index 0000000..e6bbaee --- /dev/null +++ b/Services/S7ParserService.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Threading; +using Newtonsoft.Json; +using S7Explorer.Models; +using S7Explorer.Parsers; + +namespace S7Explorer.Services +{ + public class S7ParserService + { + // Evento para notificar mensajes de log + public event EventHandler LogEvent; + + // Evento para notificar actualizaciones del árbol + public event EventHandler StructureUpdatedEvent; + + // Cancelation support + private CancellationTokenSource _cancellationTokenSource; + + // Ruta y nombre del archivo de caché JSON + private static string GetCacheFilePath(string projectFilePath) + { + return Path.ChangeExtension(projectFilePath, ".s7cache.json"); + } + + // Configuración de JSON para serialización/deserialización + private static JsonSerializerSettings JsonSettings => new JsonSerializerSettings + { + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + PreserveReferencesHandling = PreserveReferencesHandling.Objects, + TypeNameHandling = TypeNameHandling.Auto, + Formatting = Formatting.Indented + }; + + public async Task ParseProjectAsync(string projectFilePath, bool useCache = true) + { + _cancellationTokenSource = new CancellationTokenSource(); + + try + { + // Intentar cargar desde caché si useCache es true + if (useCache) + { + var cachedProject = TryLoadFromCache(projectFilePath); + if (cachedProject != null) + { + LogInfo($"Proyecto cargado desde caché: {projectFilePath}"); + return cachedProject; + } + } + + // Crear nueva instancia del proyecto + var project = new S7Project(projectFilePath); + + // Notificar al UI de la estructura base + LogInfo($"Iniciando parseo del proyecto: {projectFilePath}"); + + // Estructura básica del proyecto + var devicesFolder = new S7Object + { + Name = "Dispositivos", + ObjectType = S7ObjectType.Folder, + Parent = project + }; + project.Children.Add(devicesFolder); + + // Notificar al UI sobre la estructura inicial + NotifyStructureUpdated(project); + + // Iniciar parseo en segundo plano + await Task.Run(() => ParseProjectInBackground(project, devicesFolder, projectFilePath), + _cancellationTokenSource.Token); + + return project; + } + catch (OperationCanceledException) + { + LogWarning("Operación de parseo cancelada por el usuario"); + throw; + } + catch (Exception ex) + { + LogError($"Error general en parseo: {ex.Message}"); + throw; + } + } + + public void CancelParsing() + { + _cancellationTokenSource?.Cancel(); + } + + private void ParseProjectInBackground(S7Project project, S7Object devicesFolder, string projectFilePath) + { + // Crear instancia del parser + var parser = new S7ProjectParser(projectFilePath); + + // Conectar eventos de log + parser.LogEvent += (sender, message) => LogEvent?.Invoke(this, message); + parser.StructureUpdatedEvent += (sender, obj) => NotifyStructureUpdated(obj); + + try + { + // Parseo de dispositivos + parser.ParseDevices(devicesFolder, _cancellationTokenSource.Token); + + // Guardar en caché para futuro uso + SaveToCache(project, projectFilePath); + + LogInfo("Parseo del proyecto completado correctamente"); + } + catch (OperationCanceledException) + { + LogWarning("Parseo cancelado por el usuario"); + throw; + } + catch (Exception ex) + { + LogError($"Error en parseo: {ex.Message}"); + + // Añadir nodo de error + var errorNode = new S7Object + { + Name = "Error al parsear proyecto", + Description = ex.Message, + ObjectType = S7ObjectType.Folder, + Parent = devicesFolder + }; + devicesFolder.Children.Add(errorNode); + + // Notificar al UI + NotifyStructureUpdated(devicesFolder); + } + } + + private S7Project TryLoadFromCache(string projectFilePath) + { + string cacheFilePath = GetCacheFilePath(projectFilePath); + + try + { + if (!File.Exists(cacheFilePath)) + { + LogInfo($"No existe archivo de caché: {cacheFilePath}"); + return null; + } + + // Verificar si el archivo de caché es más reciente que el proyecto + var projectFileInfo = new FileInfo(projectFilePath); + var cacheFileInfo = new FileInfo(cacheFilePath); + + if (cacheFileInfo.LastWriteTime < projectFileInfo.LastWriteTime) + { + LogInfo("El archivo de caché es más antiguo que el proyecto. Recargando..."); + return null; + } + + LogInfo($"Cargando proyecto desde caché: {cacheFilePath}"); + string json = File.ReadAllText(cacheFilePath); + var project = JsonConvert.DeserializeObject(json, JsonSettings); + + // Reconstruir referencias padre/hijo + RebuildParentChildReferences(project); + + LogInfo("Proyecto cargado correctamente desde caché"); + return project; + } + catch (Exception ex) + { + LogWarning($"Error al cargar desde caché: {ex.Message}"); + // Si hay un error al cargar la caché, simplemente retornar null para forzar el reparseo + try + { + // Intentar eliminar el archivo de caché corrupto + if (File.Exists(cacheFilePath)) + { + File.Delete(cacheFilePath); + LogInfo("Archivo de caché corrupto eliminado"); + } + } + catch + { + // Ignorar errores al eliminar + } + return null; + } + } + + private void SaveToCache(S7Project project, string projectFilePath) + { + string cacheFilePath = GetCacheFilePath(projectFilePath); + + try + { + LogInfo($"Guardando proyecto en caché: {cacheFilePath}"); + string json = JsonConvert.SerializeObject(project, JsonSettings); + File.WriteAllText(cacheFilePath, json); + LogInfo("Proyecto guardado correctamente en caché"); + } + catch (Exception ex) + { + LogWarning($"Error al guardar proyecto en caché: {ex.Message}"); + // Continuar a pesar del error, solo es caché + } + } + + // Reconstruir referencias padre/hijo después de deserializar JSON + private void RebuildParentChildReferences(S7Object root) + { + foreach (var child in root.Children) + { + child.Parent = root; + RebuildParentChildReferences(child); + } + } + + private void NotifyStructureUpdated(S7Object obj) + { + Application.Current.Dispatcher.InvokeAsync(() => + { + StructureUpdatedEvent?.Invoke(this, obj); + }, DispatcherPriority.Background); + } + + #region Logging Methods + + private void LogInfo(string message) + { + string timestamp = DateTime.Now.ToString("HH:mm:ss.fff"); + string logMessage = $"[{timestamp}] {message}"; + LogEvent?.Invoke(this, logMessage); + } + + private void LogWarning(string message) + { + string timestamp = DateTime.Now.ToString("HH:mm:ss.fff"); + string logMessage = $"[{timestamp}] ADVERTENCIA: {message}"; + LogEvent?.Invoke(this, logMessage); + } + + private void LogError(string message) + { + string timestamp = DateTime.Now.ToString("HH:mm:ss.fff"); + string logMessage = $"[{timestamp}] ERROR: {message}"; + LogEvent?.Invoke(this, logMessage); + } + + #endregion + } +} \ No newline at end of file diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs index f01a2f4..9903ca4 100644 --- a/ViewModels/MainViewModel.cs +++ b/ViewModels/MainViewModel.cs @@ -4,6 +4,7 @@ using System.Collections.ObjectModel; using System.ComponentModel; using System.IO; using System.Linq; +using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Input; @@ -12,6 +13,7 @@ using CommunityToolkit.Mvvm.Input; using Ookii.Dialogs.Wpf; using S7Explorer.Models; using S7Explorer.Parsers; +using S7Explorer.Services; namespace S7Explorer.ViewModels { @@ -24,6 +26,9 @@ namespace S7Explorer.ViewModels private ObservableCollection _projectStructure; private string _projectInfo; private bool _isLoading; + private string _logText; + private bool _useCache = true; + private S7ParserService _parserService; public string SearchText { @@ -57,6 +62,18 @@ namespace S7Explorer.ViewModels } } + public string LogText + { + get => _logText; + set => SetProperty(ref _logText, value); + } + + public bool UseCache + { + get => _useCache; + set => SetProperty(ref _useCache, value); + } + /// /// Actualiza la información de interfaz de una función seleccionada /// @@ -67,14 +84,12 @@ namespace S7Explorer.ViewModels // Aquí podríamos cargar información adicional específica de la función // Por ejemplo, cargar el código de la función, detalles de parámetros, etc. - // Actualizar estadísticas o análisis de la función - // Por ejemplo, mostrar uso de la función en otros bloques - // Este método se ejecutará cuando el usuario seleccione una FC en el árbol + LogInfo($"Seleccionada función: {fc.Name} ({fc.Number})"); } - catch (Exception) + catch (Exception ex) { - // Ignorar errores para no interrumpir la experiencia del usuario + LogError($"Error al actualizar interfaz de función: {ex.Message}"); } } @@ -98,13 +113,37 @@ namespace S7Explorer.ViewModels public IRelayCommand OpenProjectCommand { get; } public IRelayCommand SearchCommand { get; } + public IRelayCommand ClearLogCommand { get; } + public IRelayCommand RefreshProjectCommand { get; } // Cambiado a string + public IRelayCommand CancelLoadingCommand { get; } public MainViewModel() { ProjectStructure = new ObservableCollection(); OpenProjectCommand = new RelayCommand(OpenProject); SearchCommand = new RelayCommand(SearchInProject); + ClearLogCommand = new RelayCommand(ClearLog); + RefreshProjectCommand = new RelayCommand(RefreshProject); // Cambiado a string + CancelLoadingCommand = new RelayCommand(CancelLoading); + + _parserService = new S7ParserService(); + _parserService.LogEvent += OnParserLogEvent; + _parserService.StructureUpdatedEvent += OnStructureUpdated; + ProjectInfo = "No hay proyecto abierto"; + LogText = "S7 Project Explorer iniciado. Versión 1.0\r\n"; + } + + private void RefreshProject(string useCacheString) + { + // Convertir el string a booleano + bool useCache = useCacheString == "True"; + + if (_currentProject != null) + { + UseCache = useCache; + LoadProjectAsync(_currentProject.FilePath); + } } private void OpenProject() @@ -122,33 +161,77 @@ namespace S7Explorer.ViewModels } } + private void RefreshProject(bool useCache) + { + if (_currentProject != null) + { + UseCache = useCache; + LoadProjectAsync(_currentProject.FilePath); + } + } + + private void CancelLoading() + { + try + { + if (IsLoading) + { + LogWarning("Cancelando operación de carga por petición del usuario..."); + _parserService.CancelParsing(); + } + } + catch (Exception ex) + { + LogError($"Error al cancelar la operación: {ex.Message}"); + } + } + private async void LoadProjectAsync(string filePath) { try { IsLoading = true; ProjectInfo = "Cargando proyecto..."; + LogInfo($"Iniciando carga del proyecto: {filePath}"); // Reiniciar estado ProjectStructure.Clear(); _currentProject = null; // Parsear proyecto - var parser = new S7ProjectParser(filePath); - _currentProject = await parser.ParseProjectAsync(); + _currentProject = await _parserService.ParseProjectAsync(filePath, UseCache); // Actualizar UI ProjectStructure.Add(_currentProject); - _currentProject.IsExpanded = true; + + // Expandir nodo raíz si no estamos cargando desde caché (ya que la caché preserva el estado de expansión) + if (!UseCache) + { + _currentProject.IsExpanded = true; + } // Actualizar info - ProjectInfo = $"Proyecto: {Path.GetFileNameWithoutExtension(filePath)}"; + string projectName = Path.GetFileNameWithoutExtension(filePath); + ProjectInfo = $"Proyecto: {projectName}"; + LogInfo($"Proyecto cargado: {projectName}"); + } + catch (OperationCanceledException) + { + // La operación fue cancelada + ProjectInfo = "Carga cancelada"; + LogWarning("Operación de carga cancelada por el usuario"); } catch (Exception ex) { MessageBox.Show($"Error al cargar el proyecto: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); ProjectInfo = "Error al cargar el proyecto"; + LogError($"Error al cargar proyecto: {ex.Message}"); + + if (ex.InnerException != null) + { + LogError($" Detalle: {ex.InnerException.Message}"); + } } finally { @@ -156,6 +239,24 @@ namespace S7Explorer.ViewModels } } + private void OnParserLogEvent(object sender, string message) + { + // Este evento se desencadena en un hilo de fondo, así que aseguramos + // que se ejecute en el hilo de la UI + Application.Current.Dispatcher.Invoke(() => + { + LogText += message + Environment.NewLine; + }); + } + + private void OnStructureUpdated(object sender, S7Object updatedObject) + { + // Este evento ya debe estar en el hilo de la UI + // No necesitamos hacer nada especial aquí, ya que la actualización + // se refleja automáticamente en el árbol gracias a la implementación de INPC + // y ObservableCollection + } + private void SearchInProject() { if (_currentProject == null || string.IsNullOrWhiteSpace(SearchText)) @@ -163,6 +264,8 @@ namespace S7Explorer.ViewModels try { + LogInfo($"Buscando: \"{SearchText}\""); + // Recopilar todos los objetos en el proyecto var allObjects = GetAllObjects(_currentProject); @@ -180,7 +283,7 @@ namespace S7Explorer.ViewModels { // Si es una función específica (por ejemplo "fc5") if (obj is S7Function func && - func.Number.ToLowerInvariant() == searchText) + func.Number?.ToLowerInvariant() == searchText) { matchingObjects.Add(func); } @@ -205,9 +308,9 @@ namespace S7Explorer.ViewModels // Buscar en los parámetros de la función foreach (var param in func.Parameters) { - if (param.Name.ToLowerInvariant().Contains(searchText) || - param.DataType.ToLowerInvariant().Contains(searchText) || - (param.Description?.ToLowerInvariant().Contains(searchText) ?? false)) + if (param.Name?.ToLowerInvariant().Contains(searchText) == true || + param.DataType?.ToLowerInvariant().Contains(searchText) == true || + (param.Description?.ToLowerInvariant().Contains(searchText) == true)) { matchingObjects.Add(func); break; @@ -224,7 +327,8 @@ namespace S7Explorer.ViewModels if (matchingObjects.Count == 0) { - MessageBox.Show($"No se encontraron coincidencias para: {SearchText}", + LogInfo($"No se encontraron coincidencias para: \"{SearchText}\""); + MessageBox.Show($"No se encontraron coincidencias para: \"{SearchText}\"", "Búsqueda", MessageBoxButton.OK, MessageBoxImage.Information); return; } @@ -232,10 +336,12 @@ namespace S7Explorer.ViewModels // Seleccionar el primer objeto coincidente y expandir su ruta var firstMatch = matchingObjects.First(); SelectAndExpandToObject(firstMatch); + LogInfo($"Encontrado: {firstMatch.Name} ({firstMatch.ObjectType})"); // Informar al usuario if (matchingObjects.Count > 1) { + LogInfo($"Se encontraron {matchingObjects.Count} coincidencias en total."); MessageBox.Show($"Se encontraron {matchingObjects.Count} coincidencias. " + "Mostrando la primera coincidencia.", "Búsqueda", MessageBoxButton.OK, MessageBoxImage.Information); @@ -243,6 +349,7 @@ namespace S7Explorer.ViewModels } catch (Exception ex) { + LogError($"Error durante la búsqueda: {ex.Message}"); MessageBox.Show($"Error durante la búsqueda: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); } @@ -276,5 +383,39 @@ namespace S7Explorer.ViewModels // Seleccionar el objeto SelectedTreeItem = obj; } + + private void ClearLog() + { + LogText = string.Empty; + LogInfo("Log limpiado por el usuario"); + } + + #region Logging Methods + + public void LogInfo(string message) + { + string timestamp = DateTime.Now.ToString("HH:mm:ss"); + LogText += $"[{timestamp}] INFO: {message}{Environment.NewLine}"; + } + + public void LogWarning(string message) + { + string timestamp = DateTime.Now.ToString("HH:mm:ss"); + LogText += $"[{timestamp}] ADVERTENCIA: {message}{Environment.NewLine}"; + } + + public void LogError(string message) + { + string timestamp = DateTime.Now.ToString("HH:mm:ss"); + LogText += $"[{timestamp}] ERROR: {message}{Environment.NewLine}"; + } + + public void LogDebug(string message) + { + string timestamp = DateTime.Now.ToString("HH:mm:ss"); + LogText += $"[{timestamp}] DEBUG: {message}{Environment.NewLine}"; + } + + #endregion } } \ No newline at end of file diff --git a/Views/MainWindow.xaml b/Views/MainWindow.xaml index cce7603..d4963a9 100644 --- a/Views/MainWindow.xaml +++ b/Views/MainWindow.xaml @@ -2,17 +2,26 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit" xmlns:local="clr-namespace:S7Explorer" mc:Ignorable="d" - Title="S7 Project Explorer" Height="600" Width="900"> + xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit" xmlns:local="clr-namespace:S7Explorer" + xmlns:sys="clr-namespace:System;assembly=mscorlib" mc:Ignorable="d" Title="S7 Project Explorer" Height="700" + Width="1000"> + + + + + + + + @@ -22,12 +31,20 @@