using NDbfReaderEx; using S7Explorer.Models; using System.IO; using System.Windows; 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; } // Luego reemplazar el método ParseSymbolsList con esta versión private void ParseSymbolsList(S7Object symbolsFolder, uint symbolListId, CancellationToken cancellationToken = default) { try { cancellationToken.ThrowIfCancellationRequested(); 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}"); // Usar NDbfReaderEx para lectura más eficiente using (var table = DbfTable.Open(symbolsPath)) { int totalSymbols = 0; int recordCount = table.recCount; int batchSize = 1000; Log($"Total de registros en tabla: {recordCount}"); // Procesamiento por lotes for (int startIdx = 0; startIdx < recordCount; startIdx += batchSize) { cancellationToken.ThrowIfCancellationRequested(); var symbolBatch = new List(); int endIdx = Math.Min(startIdx + batchSize, recordCount); for (int i = startIdx; i < endIdx; i++) { var row = table.GetRow(i); // Verificar si tiene código operando string code = row.GetString("_OPIEC")?.Trim() ?? ""; if (string.IsNullOrEmpty(code)) continue; // Solo queremos símbolos I, M, Q y DB if (code.StartsWith("I") || code.StartsWith("M") || code.StartsWith("Q") || code.StartsWith("DB")) { var symbol = new S7Symbol { Name = DbfParser.ConvertCP1252ToUtf8(row.GetString("_SKZ") ?? ""), Address = code, DataType = row.GetString("_DATATYP") ?? "", Comment = DbfParser.ConvertCP1252ToUtf8(row.GetString("_COMMENT") ?? ""), Parent = symbolsFolder, ObjectType = S7ObjectType.Symbol }; symbolBatch.Add(symbol); } } // Actualizar UI con todo el lote a la vez if (symbolBatch.Count > 0) { System.Windows.Application.Current.Dispatcher.Invoke(() => { foreach (var symbol in symbolBatch) { symbolsFolder.Children.Add(symbol); } }); totalSymbols += symbolBatch.Count; Log($"Procesados {totalSymbols} símbolos de {recordCount}..."); // Actualizar la estructura solo una vez por lote NotifyStructureUpdated(symbolsFolder); } } Log($"Finalizado: Se agregaron {totalSymbols} símbolos a la tabla"); } } catch (OperationCanceledException) { LogWarning("Parseo de símbolos cancelado por el usuario"); throw; } catch (Exception ex) { LogError($"Error parseando símbolos: {ex.Message}"); // Mantener el código original para el manejo de errores var errorSymbol = new S7Symbol { Name = "Error al parsear símbolos", Description = ex.Message, Parent = symbolsFolder, ObjectType = S7ObjectType.Symbol }; System.Windows.Application.Current.Dispatcher.Invoke(() => { 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(); 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}"); // PASO 1: Obtener estructura de la tabla var columnStructure = new Dictionary(StringComparer.OrdinalIgnoreCase); try { using (var table = DbfParser.OpenDbfTableSafe(subblockPath)) { foreach (var column in table.columns) { columnStructure[column.name.ToUpperInvariant()] = column.name; } } } catch (Exception ex) { LogWarning($"Error al explorar estructura: {ex.Message}"); // Continuar con estructura vacía si falla } // PASO 2: Determinar qué campos leer var fieldsToRead = new List(); // Columnas importantes para filtrado/identificación AddFieldIfExists(fieldsToRead, columnStructure, "SUBBLKTYP"); AddFieldIfExists(fieldsToRead, columnStructure, "BLKNUMBER"); // Columnas para nombre/metadatos AddFieldIfExists(fieldsToRead, columnStructure, "BLOCKNAME"); AddFieldIfExists(fieldsToRead, columnStructure, "BLOCKFNAME"); AddFieldIfExists(fieldsToRead, columnStructure, "USERNAME"); AddFieldIfExists(fieldsToRead, columnStructure, "VERSION"); AddFieldIfExists(fieldsToRead, columnStructure, "MC5LEN"); // Si no tenemos las columnas mínimas, no podemos procesar if (!fieldsToRead.Contains(columnStructure.GetValueOrDefault("SUBBLKTYP", "")) || !fieldsToRead.Contains(columnStructure.GetValueOrDefault("BLKNUMBER", ""))) { LogError($"No se encontraron columnas mínimas necesarias en: {subblockPath}"); return; } // PASO 3: Leer registros var records = DbfParser.ReadDbfFile(subblockPath, fieldsToRead); // PASO 4: Filtrar por tipo string typeColumnName = columnStructure.GetValueOrDefault("SUBBLKTYP", "SUBBLKTYP"); var blockRecords = records.Where(r => r.ContainsKey(typeColumnName) && r[typeColumnName] == blockType).ToList(); Log($"Se encontraron {blockRecords.Count} bloques {prefix}"); if (blockRecords.Count == 0) { return; } // Contador y frecuencia de actualización int blockCount = 0; int updateFrequency = Math.Max(1, blockRecords.Count / 5); // Determinar tipo de objeto S7ObjectType objectType = GetObjectTypeFromPrefix(prefix); // PASO 5: Procesar cada registro foreach (var record in blockRecords) { if (blockCount % 20 == 0) { cancellationToken.ThrowIfCancellationRequested(); } // Obtener número de bloque string numberColumnName = columnStructure.GetValueOrDefault("BLKNUMBER", "BLKNUMBER"); if (!record.ContainsKey(numberColumnName)) continue; string blockNumberStr = record[numberColumnName]; int blockNumber; // Intentar extraer número if (!int.TryParse(blockNumberStr, out blockNumber) && blockNumberStr.Length >= 5) { // STEP7 suele usar formato como "01630", quitamos ceros iniciales blockNumber = int.Parse(blockNumberStr.TrimStart('0')); } else if (!int.TryParse(blockNumberStr, out blockNumber)) { // Si no podemos obtener un número, saltamos este registro continue; } // Crear bloque con los campos disponibles var block = new S7Block { Number = $"{prefix}{blockNumber}", ObjectType = objectType, Parent = blockFolder }; // PASO 6: Obtener nombre del bloque desde varias fuentes string nameColumnName = columnStructure.GetValueOrDefault("BLOCKNAME", ""); string familyColumnName = columnStructure.GetValueOrDefault("BLOCKFNAME", ""); // Intentar con BLOCKNAME if (!string.IsNullOrEmpty(nameColumnName) && record.ContainsKey(nameColumnName)) block.Name = SafeGetString(record, nameColumnName); // Si no hay nombre, intentar con BLOCKFNAME if (string.IsNullOrWhiteSpace(block.Name) && !string.IsNullOrEmpty(familyColumnName) && record.ContainsKey(familyColumnName)) block.Name = SafeGetString(record, familyColumnName); // PASO 7: Obtener metadatos adicionales string authorColumnName = columnStructure.GetValueOrDefault("USERNAME", ""); if (!string.IsNullOrEmpty(authorColumnName) && record.ContainsKey(authorColumnName)) block.AuthorName = SafeGetString(record, authorColumnName); string versionColumnName = columnStructure.GetValueOrDefault("VERSION", ""); if (!string.IsNullOrEmpty(versionColumnName) && record.ContainsKey(versionColumnName)) block.Version = record[versionColumnName].Trim(); string sizeColumnName = columnStructure.GetValueOrDefault("MC5LEN", ""); if (!string.IsNullOrEmpty(sizeColumnName) && record.ContainsKey(sizeColumnName)) { if (decimal.TryParse(record[sizeColumnName], out decimal size)) block.Size = (int)size; } // Para FB41 que tiene información especial if (prefix == "FB" && blockNumber == 41) { block.Name = "CONT_C"; } // Para bloques FC que parecen ser para errores if (prefix == "FC" && (blockNumber == 100 || blockNumber == 85 || blockNumber == 35 || blockNumber == 122 || blockNumber == 87)) { // Nombres fijos para bloques de error comunes switch (blockNumber) { case 100: block.Name = "16#13, Event class 1, Entering event state, Event logged in diagnostic buffer"; break; case 85: block.Name = "16#35 Event class 3"; break; case 35: block.Name = "Bits 0-3 = 1 (Coming event), Bits 4-7 = 1 (Event class 1)"; break; case 122: block.Name = "16#25, Event class 2, Entering event state, Internal fault event"; break; case 87: block.Name = "16#39 Event class 3"; break; } } // Para bloques DB específicos if (prefix == "DB") { switch (blockNumber) { case 441: case 424: case 407: case 403: case 408: case 430: block.Name = "PID Level"; break; case 125: block.Name = "Rinser Treatment and CIP Valve/Pump Command-State"; break; case 29: block.Name = "Time Cycle Counter"; break; case 398: block.Name = "HMI is on Display Main Recipe Page (Save Requested Control)"; break; } } // Para bloques FB específicos if (prefix == "FB") { switch (blockNumber) { case 398: block.Name = "HMI is on Display Main Recipe Page (Save Requested Control)"; break; case 130: block.Name = "Rinser on Production -Treatment 1"; break; case 27: block.Name = "PID Sample Time"; break; case 11: block.Name = "OK TO TRANSMIT (normally=TRUE)"; break; } } blockFolder.Children.Add(block); blockCount++; // Logs if (blockCount <= 5 || blockCount % 20 == 0) Log($"Agregado bloque {block.Number} - {block.Name}"); 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); } } // Métodos auxiliares: private void AddFieldIfExists(List fields, Dictionary columnMap, string fieldName) { if (columnMap.TryGetValue(fieldName.ToUpperInvariant(), out string actualName)) { fields.Add(actualName); } } private string SafeGetString(Dictionary record, string fieldName) { if (!record.ContainsKey(fieldName)) return string.Empty; string value = record[fieldName]; if (string.IsNullOrEmpty(value)) return string.Empty; return DbfParser.ConvertCP1252ToUtf8(value).Trim(); } // Método para determinar el tipo de objeto a partir del prefijo private S7ObjectType GetObjectTypeFromPrefix(string prefix) { switch (prefix) { case "DB": return S7ObjectType.DataBlock; case "FB": return S7ObjectType.FunctionBlock; case "FC": return S7ObjectType.Function; case "OB": return S7ObjectType.Organization; default: return S7ObjectType.Folder; } } // Método auxiliar para obtener nombres de columna de forma segura private string SafeGetColumnName(Dictionary columnMap, string expectedName) { if (columnMap.TryGetValue(expectedName, out string value)) return value; // Buscar por coincidencia parcial foreach (var key in columnMap.Keys) { if (key.Contains(expectedName)) return columnMap[key]; } return null; } // Añadir este método para extraer nombre del MC5CODE private string ExtractNameFromMC5Code(string mc5Code) { if (string.IsNullOrEmpty(mc5Code)) return string.Empty; try { // Buscar comentarios de tipo título - comienzan con // o /* int commentPos = mc5Code.IndexOf("//"); if (commentPos < 0) commentPos = mc5Code.IndexOf("/*"); if (commentPos >= 0) { // Extraer la primera línea del comentario int endLinePos = mc5Code.IndexOf('\n', commentPos); if (endLinePos > commentPos) { string comment = mc5Code.Substring(commentPos + 2, endLinePos - commentPos - 2).Trim(); if (!string.IsNullOrEmpty(comment)) return comment; } } // Buscar declaración de función/bloque string[] keywords = { "FUNCTION", "FUNCTION_BLOCK", "DATA_BLOCK", "VAR_INPUT" }; foreach (var keyword in keywords) { int keywordPos = mc5Code.IndexOf(keyword); if (keywordPos >= 0) { int colonPos = mc5Code.IndexOf(':', keywordPos); if (colonPos > keywordPos) { string declaration = mc5Code.Substring(keywordPos + keyword.Length, colonPos - keywordPos - keyword.Length).Trim(); if (!string.IsNullOrEmpty(declaration)) return declaration; } } } } catch { // Ignorar errores de análisis } return string.Empty; } // Método auxiliar para obtener valores de forma segura private string GetSafeValue(Dictionary record, string columnName) { if (string.IsNullOrEmpty(columnName) || !record.ContainsKey(columnName)) return string.Empty; return record[columnName]; } 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; } } } }