S7Explorer/Parsers/S7ProjectParser.cs

564 lines
23 KiB
C#

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<string> LogEvent;
// Evento para notificar actualizaciones del árbol
public event EventHandler<S7Object> 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<DeviceIdInfo> ParseDeviceIdInfos()
{
var result = new List<DeviceIdInfo>();
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; }
}
}
}