822 lines
34 KiB
C#
822 lines
34 KiB
C#
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<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;
|
|
}
|
|
|
|
|
|
// 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<S7Symbol>();
|
|
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<string, string>(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<string>();
|
|
// 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<string> fields, Dictionary<string, string> columnMap, string fieldName)
|
|
{
|
|
if (columnMap.TryGetValue(fieldName.ToUpperInvariant(), out string actualName))
|
|
{
|
|
fields.Add(actualName);
|
|
}
|
|
}
|
|
|
|
private string SafeGetString(Dictionary<string, string> 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<string, string> 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<string, string> 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; }
|
|
}
|
|
}
|
|
} |