using System; 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 { public class S7ProjectParser { 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; _projectDirectory = Path.GetDirectoryName(projectFilePath); if (string.IsNullOrEmpty(_projectDirectory)) throw new ArgumentException("No se pudo determinar el directorio del proyecto"); } public void ParseDevices(S7Object devicesFolder, CancellationToken cancellationToken = default) { try { 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, ObjectType = S7ObjectType.Device, Parent = devicesFolder }; 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"); // 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) { 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) { 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", Description = ex.Message, ObjectType = S7ObjectType.Folder, Parent = devicesFolder }; devicesFolder.Children.Add(errorNode); NotifyStructureUpdated(devicesFolder); } } private S7Object CreateBlockFolder(S7Object parent, string name) { var folder = new S7Object { Name = name, ObjectType = S7ObjectType.Folder, Parent = parent }; parent.Children.Add(folder); return folder; } private List ParseDeviceIdInfos() { var result = new List(); try { // Parsear S7RESOFF.DBF var s7resoffPath = Path.Combine(_projectDirectory, "hrs", "S7RESOFF.DBF"); if (File.Exists(s7resoffPath)) { 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) { var device = new DeviceIdInfo { Name = DbfParser.ConvertCP1252ToUtf8(record["NAME"]), }; // 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}"); // 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); } } else { 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 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 = Path.GetFileNameWithoutExtension(_projectFilePath), SubblockListId = 0, SymbolListId = 0 }); } return result; } private void ParseSymbolsList(S7Object symbolsFolder, uint symbolListId, CancellationToken cancellationToken = default) { try { // Verificar cancelación cancellationToken.ThrowIfCancellationRequested(); // Construir la ruta del archivo de símbolos basada en SymbolListId string symbolsPath = FindSymbolsPath(symbolListId); if (string.IsNullOrEmpty(symbolsPath)) { LogWarning($"No se pudo encontrar el archivo de símbolos para ID: 0x{symbolListId:X8}"); return; } 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) { LogError($"Error parseando símbolos: {ex.Message}"); var errorSymbol = new S7Symbol { Name = "Error al parsear símbolos", Description = ex.Message, Parent = symbolsFolder, ObjectType = S7ObjectType.Symbol }; symbolsFolder.Children.Add(errorSymbol); NotifyStructureUpdated(symbolsFolder); } } 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)) { 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 { 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)) { 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" }); // 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; } // Contador de bloques int blockCount = 0; int updateFrequency = Math.Max(1, blockRecords.Count / 5); // Actualizar UI cada 20% aprox // 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; } // Procesar cada registro foreach (var record in blockRecords) { // Verificar cancelación periódicamente if (blockCount % 20 == 0) { cancellationToken.ThrowIfCancellationRequested(); } // 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); } } } // Ordenar bloques por número SortBlocksInFolder(blockFolder); Log($"Completado procesamiento de {blockCount} bloques {prefix}"); } catch (OperationCanceledException) { 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 SortBlocksInFolder(S7Object folder) { // Crear una nueva lista ordenada var sortedList = folder.Children .OrderBy(block => { if (block.Number == null) return int.MaxValue; 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) { 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 uint? SubblockListId { get; set; } public uint? SymbolListId { get; set; } } } }