Funciones basicas
|
@ -11,6 +11,9 @@ namespace S7Explorer
|
||||||
{
|
{
|
||||||
base.OnStartup(e);
|
base.OnStartup(e);
|
||||||
|
|
||||||
|
// AÑADIR ESTO: Registrar proveedor de codificación para soportar codificaciones adicionales (CP850, etc.)
|
||||||
|
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
|
||||||
|
|
||||||
// Asegurarnos de que existe la carpeta Resources para los íconos
|
// Asegurarnos de que existe la carpeta Resources para los íconos
|
||||||
EnsureResourcesExist();
|
EnsureResourcesExist();
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,22 @@ namespace S7Explorer.Models
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
public string Number { get; set; }
|
public string Number { get; set; }
|
||||||
|
|
||||||
|
// En S7Object.cs
|
||||||
|
[Browsable(false)]
|
||||||
|
public string DisplayName
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(Number))
|
||||||
|
return Name;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(Name))
|
||||||
|
return Number;
|
||||||
|
|
||||||
|
return $"{Number} - {Name}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[DisplayName("Nombre")]
|
[DisplayName("Nombre")]
|
||||||
public string Name
|
public string Name
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,14 +2,23 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using NDbfReader;
|
using System.Linq;
|
||||||
|
using NDbfReaderEx; // Cambiar referencia de librería
|
||||||
|
|
||||||
namespace S7Explorer.Parsers
|
namespace S7Explorer.Parsers
|
||||||
{
|
{
|
||||||
public class DbfParser
|
public class DbfParser
|
||||||
{
|
{
|
||||||
// Lee campos específicos de un archivo DBF
|
// Cache de codificación para reducir instanciación repetida
|
||||||
public static List<Dictionary<string, string>> ReadDbfFile(string filePath, IEnumerable<string> fieldNames)
|
private static readonly Encoding Windows1252 = Encoding.GetEncoding(1252);
|
||||||
|
private static readonly Encoding Utf8 = Encoding.UTF8;
|
||||||
|
|
||||||
|
// Lee campos específicos de un archivo DBF, puede leer por lotes si se especifica
|
||||||
|
public static List<Dictionary<string, string>> ReadDbfFile(
|
||||||
|
string filePath,
|
||||||
|
IEnumerable<string> fieldNames,
|
||||||
|
int maxRecords = int.MaxValue,
|
||||||
|
int startRecord = 0)
|
||||||
{
|
{
|
||||||
var result = new List<Dictionary<string, string>>();
|
var result = new List<Dictionary<string, string>>();
|
||||||
|
|
||||||
|
@ -18,41 +27,69 @@ namespace S7Explorer.Parsers
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Abrir tabla DBF con codificación específica (importante para caracteres especiales en STEP7)
|
// Usar nuestra función mejorada para abrir el archivo
|
||||||
using var stream = File.OpenRead(filePath);
|
using (var table = OpenDbfTableSafe(filePath))
|
||||||
using var table = Table.Open(stream);
|
|
||||||
|
|
||||||
// Crear lector usando la API correcta
|
|
||||||
var reader = table.OpenReader();
|
|
||||||
|
|
||||||
while (reader.Read())
|
|
||||||
{
|
{
|
||||||
var record = new Dictionary<string, string>();
|
int estimatedSize = Math.Min(1000, maxRecords);
|
||||||
|
result = new List<Dictionary<string, string>>(estimatedSize);
|
||||||
|
|
||||||
|
// Filtrar campos memo para evitar errores
|
||||||
|
var safeFields = new List<string>();
|
||||||
foreach (var fieldName in fieldNames)
|
foreach (var fieldName in fieldNames)
|
||||||
{
|
{
|
||||||
// Obtener valor y convertir a string si no es null
|
// Verificar si el campo es memo
|
||||||
var value = reader.GetValue(fieldName);
|
var column = table.columns.FirstOrDefault(c => c.name.Equals(fieldName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (column != null && column.dbfType != NDbfReaderEx.NativeColumnType.Memo)
|
||||||
// Manejar los diferentes tipos de datos
|
|
||||||
if (value is byte[] byteValue)
|
|
||||||
{
|
{
|
||||||
// Para campos de tipo binario como MC5CODE
|
safeFields.Add(fieldName);
|
||||||
record[fieldName] = Encoding.GetEncoding(1252).GetString(byteValue);
|
|
||||||
}
|
}
|
||||||
else if (value is DateTime dateValue)
|
else if (column == null)
|
||||||
{
|
{
|
||||||
// Mantener formato consistente para fechas
|
// Si el campo no existe, lo incluimos para que sea manejado por el código posterior
|
||||||
record[fieldName] = dateValue.ToString("yyyy-MM-dd HH:mm:ss");
|
safeFields.Add(fieldName);
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Para todos los demás tipos
|
|
||||||
record[fieldName] = value?.ToString() ?? string.Empty;
|
|
||||||
}
|
}
|
||||||
|
// Campos memo se omiten intencionalmente para evitar errores
|
||||||
}
|
}
|
||||||
|
|
||||||
result.Add(record);
|
for (int i = startRecord; i < startRecord + maxRecords && i < table.recCount; i++)
|
||||||
|
{
|
||||||
|
var row = table.GetRow(i);
|
||||||
|
var record = new Dictionary<string, string>(safeFields.Count, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var fieldName in safeFields)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (row.IsNull(fieldName))
|
||||||
|
{
|
||||||
|
record[fieldName] = string.Empty;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
object value = row.GetValue(fieldName);
|
||||||
|
|
||||||
|
if (value is byte[] byteValue)
|
||||||
|
{
|
||||||
|
record[fieldName] = Windows1252.GetString(byteValue);
|
||||||
|
}
|
||||||
|
else if (value is DateTime dateValue)
|
||||||
|
{
|
||||||
|
record[fieldName] = dateValue.ToString("yyyy-MM-dd HH:mm:ss");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
record[fieldName] = value?.ToString() ?? string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Si hay un error al leer este campo, simplemente ponemos un valor vacío
|
||||||
|
record[fieldName] = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Add(record);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
@ -63,25 +100,90 @@ namespace S7Explorer.Parsers
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nuevo método para lectura por lotes usando las capacidades mejoradas de NDbfReaderEx
|
||||||
|
public static IEnumerable<List<Dictionary<string, string>>> ReadDbfFileInBatches(
|
||||||
|
string filePath,
|
||||||
|
IEnumerable<string> fieldNames,
|
||||||
|
int batchSize = 1000)
|
||||||
|
{
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
throw new FileNotFoundException($"No se encontró el archivo DBF: {filePath}");
|
||||||
|
|
||||||
|
using (var table = OpenDbfTableSafe(filePath))
|
||||||
|
{
|
||||||
|
var fieldsArray = new List<string>(fieldNames).ToArray();
|
||||||
|
int totalRecords = table.recCount;
|
||||||
|
|
||||||
|
for (int startIndex = 0; startIndex < totalRecords; startIndex += batchSize)
|
||||||
|
{
|
||||||
|
var batch = new List<Dictionary<string, string>>(Math.Min(batchSize, totalRecords - startIndex));
|
||||||
|
|
||||||
|
int endIndex = Math.Min(startIndex + batchSize, totalRecords);
|
||||||
|
|
||||||
|
for (int i = startIndex; i < endIndex; i++)
|
||||||
|
{
|
||||||
|
var row = table.GetRow(i);
|
||||||
|
var record = new Dictionary<string, string>(fieldsArray.Length, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var fieldName in fieldsArray)
|
||||||
|
{
|
||||||
|
if (row.IsNull(fieldName))
|
||||||
|
{
|
||||||
|
record[fieldName] = string.Empty;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
object value = row.GetValue(fieldName);
|
||||||
|
|
||||||
|
if (value is byte[] byteValue)
|
||||||
|
{
|
||||||
|
record[fieldName] = Windows1252.GetString(byteValue);
|
||||||
|
}
|
||||||
|
else if (value is DateTime dateValue)
|
||||||
|
{
|
||||||
|
record[fieldName] = dateValue.ToString("yyyy-MM-dd HH:mm:ss");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
record[fieldName] = value?.ToString() ?? string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
batch.Add(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return batch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Convierte un string que representa un número a un entero opcional
|
// Convierte un string que representa un número a un entero opcional
|
||||||
public static int? StringToInt(string value)
|
public static int? StringToInt(string value)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
// Eliminar espacios y caracteres no numéricos iniciales
|
// Usar TryParse directamente si es posible
|
||||||
string trimmedValue = value.Trim();
|
if (int.TryParse(value, out int directResult))
|
||||||
int startIndex = 0;
|
return directResult;
|
||||||
while (startIndex < trimmedValue.Length && !char.IsDigit(trimmedValue[startIndex]))
|
|
||||||
startIndex++;
|
|
||||||
|
|
||||||
if (startIndex >= trimmedValue.Length)
|
// Si no, intentar extraer la parte numérica
|
||||||
return null;
|
string numericPart = string.Empty;
|
||||||
|
bool foundDigit = false;
|
||||||
|
|
||||||
// Extraer solo los dígitos
|
foreach (char c in value)
|
||||||
string numericPart = new string(trimmedValue.Substring(startIndex)
|
{
|
||||||
.TakeWhile(char.IsDigit)
|
if (char.IsDigit(c))
|
||||||
.ToArray());
|
{
|
||||||
|
numericPart += c;
|
||||||
|
foundDigit = true;
|
||||||
|
}
|
||||||
|
else if (foundDigit)
|
||||||
|
{
|
||||||
|
// Si ya encontramos dígitos y ahora encontramos otro carácter, terminamos
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (int.TryParse(numericPart, out int result))
|
if (int.TryParse(numericPart, out int result))
|
||||||
return result;
|
return result;
|
||||||
|
@ -89,7 +191,7 @@ namespace S7Explorer.Parsers
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convierte códigos Windows-1252 a UTF-8 para manejar caracteres especiales en STEP7
|
// Optimizado convertidor de codificación
|
||||||
public static string ConvertCP1252ToUtf8(string input)
|
public static string ConvertCP1252ToUtf8(string input)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(input))
|
if (string.IsNullOrEmpty(input))
|
||||||
|
@ -97,20 +199,35 @@ namespace S7Explorer.Parsers
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Primero decodificar como Windows-1252 y luego encodear como UTF-8
|
// Optimización: Si solo contiene caracteres ASCII, no necesitamos conversión
|
||||||
byte[] bytes = Encoding.GetEncoding(1252).GetBytes(input);
|
bool needsConversion = false;
|
||||||
return Encoding.UTF8.GetString(bytes);
|
for (int i = 0; i < input.Length; i++)
|
||||||
|
{
|
||||||
|
if (input[i] > 127)
|
||||||
|
{
|
||||||
|
needsConversion = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!needsConversion)
|
||||||
|
return input;
|
||||||
|
|
||||||
|
// Solo realizar conversión para texto con caracteres no-ASCII
|
||||||
|
byte[] bytes = Windows1252.GetBytes(input);
|
||||||
|
return Utf8.GetString(bytes);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// En caso de error, devolver el string original
|
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Los demás métodos se mantienen igual
|
||||||
// Busca un archivo DBF en varias ubicaciones posibles basadas en el patrón de archivos STEP7
|
// Busca un archivo DBF en varias ubicaciones posibles basadas en el patrón de archivos STEP7
|
||||||
public static string FindDbfFile(string basePath, string relativePath)
|
public static string FindDbfFile(string basePath, string relativePath)
|
||||||
{
|
{
|
||||||
|
// Ruta directa
|
||||||
string path = Path.Combine(basePath, relativePath);
|
string path = Path.Combine(basePath, relativePath);
|
||||||
if (File.Exists(path))
|
if (File.Exists(path))
|
||||||
return path;
|
return path;
|
||||||
|
@ -124,16 +241,164 @@ namespace S7Explorer.Parsers
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Intentar buscar por nombre de archivo en subdirectorios
|
// Obtener solo el nombre del archivo sin ruta
|
||||||
string fileName = Path.GetFileName(relativePath);
|
string fileName = Path.GetFileName(relativePath);
|
||||||
foreach (var subdir in Directory.GetDirectories(basePath, "*", SearchOption.AllDirectories))
|
|
||||||
|
// Búsqueda en profundidad pero con límite para evitar que sea demasiado lenta
|
||||||
|
return FindFileWithDepthLimit(basePath, fileName, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Método de búsqueda con límite de profundidad
|
||||||
|
private static string FindFileWithDepthLimit(string directory, string fileName, int maxDepth)
|
||||||
|
{
|
||||||
|
if (maxDepth <= 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
path = Path.Combine(subdir, fileName);
|
// Buscar en este directorio
|
||||||
if (File.Exists(path))
|
string filePath = Path.Combine(directory, fileName);
|
||||||
return path;
|
if (File.Exists(filePath))
|
||||||
|
return filePath;
|
||||||
|
|
||||||
|
// Buscar en subdirectorios con profundidad reducida
|
||||||
|
foreach (var subdir in Directory.GetDirectories(directory))
|
||||||
|
{
|
||||||
|
string result = FindFileWithDepthLimit(subdir, fileName, maxDepth - 1);
|
||||||
|
if (result != null)
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignorar errores de acceso a directorios
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Agregar al archivo DbfParser.cs
|
||||||
|
public static Dictionary<string, string> ExploreDbfStructure(string filePath)
|
||||||
|
{
|
||||||
|
var result = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var table = OpenDbfTableSafe(filePath))
|
||||||
|
{
|
||||||
|
foreach (var column in table.columns)
|
||||||
|
{
|
||||||
|
result[column.name.ToUpperInvariant()] = column.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error explorando estructura: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void LogColumnInfo(string filePath, Action<string> logMethod, int maxRows = 3)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var table = OpenDbfTableSafe(filePath))
|
||||||
|
{
|
||||||
|
logMethod($"Archivo: {filePath}");
|
||||||
|
logMethod($"Número de columnas: {table.columns.Count}");
|
||||||
|
|
||||||
|
// Mostrar información de columnas usando las propiedades correctas
|
||||||
|
logMethod("Columnas:");
|
||||||
|
foreach (var column in table.columns)
|
||||||
|
{
|
||||||
|
logMethod($" Nombre: {column.name}, Tipo DBF: {column.dbfType}, Tipo .NET: {column.type}");
|
||||||
|
logMethod($" Tamaño: {column.size}, Decimales: {column.dec}, Ancho: {column.displayWidth}");
|
||||||
|
logMethod($" Alineación: {(column.leftSideDisplay ? "Izquierda" : "Derecha")}");
|
||||||
|
logMethod(" -----");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mostrar algunos valores de muestra
|
||||||
|
logMethod("\nValores de muestra:");
|
||||||
|
for (int i = 0; i < Math.Min(maxRows, table.recCount); i++)
|
||||||
|
{
|
||||||
|
var row = table.GetRow(i);
|
||||||
|
logMethod($"Registro {i}:");
|
||||||
|
|
||||||
|
foreach (var column in table.columns)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
object value = row.GetValue(column.name);
|
||||||
|
string valueStr = value != null ? value.ToString() : "null";
|
||||||
|
|
||||||
|
// Limitar longitud para no saturar el log
|
||||||
|
if (valueStr.Length > 100)
|
||||||
|
valueStr = valueStr.Substring(0, 97) + "...";
|
||||||
|
|
||||||
|
logMethod($" {column.name}: {valueStr}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logMethod($" {column.name}: ERROR - {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logMethod(string.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logMethod($"Error explorando columnas: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DbfTable OpenDbfTableSafe(string filePath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Primera opción: abrir normalmente sin campos memo
|
||||||
|
return DbfTable.Open(filePath, null, false);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex.Message.Contains("encoding"))
|
||||||
|
{
|
||||||
|
// Problemas de codificación, probar con codificaciones específicas
|
||||||
|
try { return DbfTable.Open(filePath, Encoding.GetEncoding(1252), false); }
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
try { return DbfTable.Open(filePath, Encoding.GetEncoding(850), false); }
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
try { return DbfTable.Open(filePath, Encoding.GetEncoding(437), false); }
|
||||||
|
catch { return DbfTable.Open(filePath, Encoding.Default, false); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex.Message.Contains("ReadMemoBytes") || ex.Message.Contains("block number"))
|
||||||
|
{
|
||||||
|
// Problemas específicos con campos memo - intentar con diferentes tipos de DBF
|
||||||
|
try { return DbfTable.Open(filePath, Encoding.GetEncoding(1252), false, StrictHeader.none, DbfTableType.DBF_Ver3); }
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
try { return DbfTable.Open(filePath, Encoding.GetEncoding(1252), false, StrictHeader.none, DbfTableType.DBF_Ver3_dBase); }
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
try { return DbfTable.Open(filePath, Encoding.GetEncoding(1252), false, StrictHeader.none, DbfTableType.DBF_Ver3_Clipper); }
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
try { return DbfTable.Open(filePath, Encoding.GetEncoding(1252), false, StrictHeader.none, DbfTableType.DBF_Ver4); }
|
||||||
|
catch { return DbfTable.Open(filePath, Encoding.Default, false, StrictHeader.none, DbfTableType.DBF_Ver3); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Último recurso: intentar abrir con configuraciones extremadamente permisivas
|
||||||
|
return DbfTable.Open(filePath, Encoding.Default, false, StrictHeader.none);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,13 +1,7 @@
|
||||||
using System;
|
using NDbfReaderEx;
|
||||||
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;
|
using S7Explorer.Models;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
namespace S7Explorer.Parsers
|
namespace S7Explorer.Parsers
|
||||||
{
|
{
|
||||||
|
@ -245,14 +239,14 @@ namespace S7Explorer.Parsers
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Luego reemplazar el método ParseSymbolsList con esta versión
|
||||||
private void ParseSymbolsList(S7Object symbolsFolder, uint symbolListId, CancellationToken cancellationToken = default)
|
private void ParseSymbolsList(S7Object symbolsFolder, uint symbolListId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Verificar cancelación
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
// Construir la ruta del archivo de símbolos basada en SymbolListId
|
|
||||||
string symbolsPath = FindSymbolsPath(symbolListId);
|
string symbolsPath = FindSymbolsPath(symbolListId);
|
||||||
if (string.IsNullOrEmpty(symbolsPath))
|
if (string.IsNullOrEmpty(symbolsPath))
|
||||||
{
|
{
|
||||||
|
@ -262,52 +256,71 @@ namespace S7Explorer.Parsers
|
||||||
|
|
||||||
Log($"Procesando archivo de símbolos: {symbolsPath}");
|
Log($"Procesando archivo de símbolos: {symbolsPath}");
|
||||||
|
|
||||||
// Leer SYMLIST.DBF
|
// Usar NDbfReaderEx para lectura más eficiente
|
||||||
var records = DbfParser.ReadDbfFile(symbolsPath, new[] { "_SKZ", "_OPIEC", "_DATATYP", "_COMMENT" });
|
using (var table = DbfTable.Open(symbolsPath))
|
||||||
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
|
int totalSymbols = 0;
|
||||||
if (symbolCount % 50 == 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();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
}
|
|
||||||
|
|
||||||
// Solo procesar entradas con código
|
var symbolBatch = new List<S7Symbol>();
|
||||||
if (!string.IsNullOrEmpty(record["_OPIEC"]))
|
int endIdx = Math.Min(startIdx + batchSize, recordCount);
|
||||||
{
|
|
||||||
string code = record["_OPIEC"].Trim();
|
|
||||||
|
|
||||||
// Solo queremos símbolos I, M, Q y DB para la lista de símbolos
|
for (int i = startIdx; i < endIdx; i++)
|
||||||
if (code.StartsWith("I") || code.StartsWith("M") || code.StartsWith("Q") || code.StartsWith("DB"))
|
|
||||||
{
|
{
|
||||||
var symbol = new S7Symbol
|
var row = table.GetRow(i);
|
||||||
{
|
|
||||||
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
|
// Verificar si tiene código operando
|
||||||
if (symbolCount % updateFrequency == 0)
|
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"))
|
||||||
{
|
{
|
||||||
Log($"Procesados {symbolCount} símbolos...");
|
var symbol = new S7Symbol
|
||||||
NotifyStructureUpdated(symbolsFolder);
|
{
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Log($"Se agregaron {symbolCount} símbolos a la tabla de símbolos");
|
// 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)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
|
@ -317,7 +330,7 @@ namespace S7Explorer.Parsers
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
LogError($"Error parseando símbolos: {ex.Message}");
|
LogError($"Error parseando símbolos: {ex.Message}");
|
||||||
|
// Mantener el código original para el manejo de errores
|
||||||
var errorSymbol = new S7Symbol
|
var errorSymbol = new S7Symbol
|
||||||
{
|
{
|
||||||
Name = "Error al parsear símbolos",
|
Name = "Error al parsear símbolos",
|
||||||
|
@ -325,7 +338,12 @@ namespace S7Explorer.Parsers
|
||||||
Parent = symbolsFolder,
|
Parent = symbolsFolder,
|
||||||
ObjectType = S7ObjectType.Symbol
|
ObjectType = S7ObjectType.Symbol
|
||||||
};
|
};
|
||||||
symbolsFolder.Children.Add(errorSymbol);
|
|
||||||
|
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
symbolsFolder.Children.Add(errorSymbol);
|
||||||
|
});
|
||||||
|
|
||||||
NotifyStructureUpdated(symbolsFolder);
|
NotifyStructureUpdated(symbolsFolder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -378,13 +396,12 @@ namespace S7Explorer.Parsers
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ParseBlocksOfType(S7Object blockFolder, string blockType, string prefix,
|
private void ParseBlocksOfType(S7Object blockFolder, string blockType, string prefix,
|
||||||
uint subblockListId, CancellationToken cancellationToken = default)
|
uint subblockListId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
// Construir la ruta a SUBBLK.DBF
|
|
||||||
string subblockPath = Path.Combine(_projectDirectory, "ombstx", "offline",
|
string subblockPath = Path.Combine(_projectDirectory, "ombstx", "offline",
|
||||||
subblockListId.ToString("X8"), "SUBBLK.DBF");
|
subblockListId.ToString("X8"), "SUBBLK.DBF");
|
||||||
|
|
||||||
|
@ -396,15 +413,50 @@ namespace S7Explorer.Parsers
|
||||||
|
|
||||||
Log($"Procesando bloques de tipo {prefix} desde: {subblockPath}");
|
Log($"Procesando bloques de tipo {prefix} desde: {subblockPath}");
|
||||||
|
|
||||||
// Leer SUBBLK.DBF
|
// PASO 1: Obtener estructura de la tabla
|
||||||
var records = DbfParser.ReadDbfFile(subblockPath, new[]
|
var columnStructure = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
try
|
||||||
{
|
{
|
||||||
"SUBBLKTYP", "BLKNUMBER", "BLKNAME", "AUTHOR",
|
using (var table = DbfParser.OpenDbfTableSafe(subblockPath))
|
||||||
"FAMILY", "VERSION", "CREATEDATE", "MODDATE"
|
{
|
||||||
});
|
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
|
||||||
|
}
|
||||||
|
|
||||||
// Filtrar registros para este tipo de bloque
|
// PASO 2: Determinar qué campos leer
|
||||||
var blockRecords = records.Where(r => r["SUBBLKTYP"] == blockType).ToList();
|
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}");
|
Log($"Se encontraron {blockRecords.Count} bloques {prefix}");
|
||||||
|
|
||||||
if (blockRecords.Count == 0)
|
if (blockRecords.Count == 0)
|
||||||
|
@ -412,73 +464,168 @@ namespace S7Explorer.Parsers
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Contador de bloques
|
// Contador y frecuencia de actualización
|
||||||
int blockCount = 0;
|
int blockCount = 0;
|
||||||
int updateFrequency = Math.Max(1, blockRecords.Count / 5); // Actualizar UI cada 20% aprox
|
int updateFrequency = Math.Max(1, blockRecords.Count / 5);
|
||||||
|
|
||||||
// Definir tipo de objeto según prefijo
|
// Determinar tipo de objeto
|
||||||
S7ObjectType objectType;
|
S7ObjectType objectType = GetObjectTypeFromPrefix(prefix);
|
||||||
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
|
// PASO 5: Procesar cada registro
|
||||||
foreach (var record in blockRecords)
|
foreach (var record in blockRecords)
|
||||||
{
|
{
|
||||||
// Verificar cancelación periódicamente
|
|
||||||
if (blockCount % 20 == 0)
|
if (blockCount % 20 == 0)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Solo procesar si tenemos un número de bloque válido
|
// Obtener número de bloque
|
||||||
if (int.TryParse(record["BLKNUMBER"], out int blockNumber))
|
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)
|
||||||
{
|
{
|
||||||
// Crear objeto de bloque
|
// STEP7 suele usar formato como "01630", quitamos ceros iniciales
|
||||||
var block = new S7Block
|
blockNumber = int.Parse(blockNumberStr.TrimStart('0'));
|
||||||
{
|
}
|
||||||
Name = DbfParser.ConvertCP1252ToUtf8(record["BLKNAME"]),
|
else if (!int.TryParse(blockNumberStr, out blockNumber))
|
||||||
Number = $"{prefix}{blockNumber}",
|
{
|
||||||
AuthorName = DbfParser.ConvertCP1252ToUtf8(record["AUTHOR"]),
|
// Si no podemos obtener un número, saltamos este registro
|
||||||
Family = DbfParser.ConvertCP1252ToUtf8(record["FAMILY"]),
|
continue;
|
||||||
Version = record["VERSION"],
|
}
|
||||||
ObjectType = objectType,
|
|
||||||
Parent = blockFolder
|
|
||||||
};
|
|
||||||
|
|
||||||
// Intentar extraer fecha de modificación
|
// Crear bloque con los campos disponibles
|
||||||
if (DateTime.TryParse(record["MODDATE"], out DateTime modDate))
|
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)
|
||||||
{
|
{
|
||||||
block.Modified = modDate;
|
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;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Añadir a la carpeta correspondiente
|
// Para bloques DB específicos
|
||||||
blockFolder.Children.Add(block);
|
if (prefix == "DB")
|
||||||
blockCount++;
|
{
|
||||||
|
switch (blockNumber)
|
||||||
// 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}");
|
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;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Actualizar UI periódicamente para mostrar progreso
|
// Para bloques FB específicos
|
||||||
if (blockCount % updateFrequency == 0)
|
if (prefix == "FB")
|
||||||
|
{
|
||||||
|
switch (blockNumber)
|
||||||
{
|
{
|
||||||
Log($"Procesados {blockCount}/{blockRecords.Count} bloques {prefix}...");
|
case 398:
|
||||||
NotifyStructureUpdated(blockFolder);
|
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
|
// Ordenar bloques por número
|
||||||
SortBlocksInFolder(blockFolder);
|
SortBlocksInFolder(blockFolder);
|
||||||
|
|
||||||
Log($"Completado procesamiento de {blockCount} bloques {prefix}");
|
Log($"Completado procesamiento de {blockCount} bloques {prefix}");
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
|
@ -499,11 +646,122 @@ namespace S7Explorer.Parsers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
private void SortBlocksInFolder(S7Object folder)
|
||||||
{
|
{
|
||||||
// Crear una nueva lista ordenada
|
// Crear una nueva lista ordenada
|
||||||
var sortedList = folder.Children
|
var sortedList = folder.Children
|
||||||
.OrderBy(block => {
|
.OrderBy(block =>
|
||||||
|
{
|
||||||
if (block.Number == null) return int.MaxValue;
|
if (block.Number == null) return int.MaxValue;
|
||||||
|
|
||||||
string numPart = new string(block.Number
|
string numPart = new string(block.Number
|
||||||
|
|
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 7.4 KiB |
After Width: | Height: | Size: 8.4 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 9.9 KiB |
|
@ -11,9 +11,44 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||||
<PackageReference Include="Extended.Wpf.Toolkit" Version="4.7.25104.5739" />
|
<PackageReference Include="Extended.Wpf.Toolkit" Version="4.7.25104.5739" />
|
||||||
<PackageReference Include="NDbfReader" Version="2.4.0" />
|
<PackageReference Include="NDbfReaderEx" Version="1.4.1.1" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="Ookii.Dialogs.Wpf" Version="5.0.1" />
|
<PackageReference Include="Ookii.Dialogs.Wpf" Version="5.0.1" />
|
||||||
|
<PackageReference Include="System.Text.Encoding.CodePages" Version="10.0.0-preview.2.25163.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Resources\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="Resources\db.png">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="Resources\default.png">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="Resources\device.png">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="Resources\fb.png">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="Resources\fc.png">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="Resources\folder.png">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="Resources\ob.png">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="Resources\project.png">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="Resources\symbol.png">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -116,6 +116,7 @@ namespace S7Explorer.ViewModels
|
||||||
public IRelayCommand ClearLogCommand { get; }
|
public IRelayCommand ClearLogCommand { get; }
|
||||||
public IRelayCommand<string> RefreshProjectCommand { get; } // Cambiado a string
|
public IRelayCommand<string> RefreshProjectCommand { get; } // Cambiado a string
|
||||||
public IRelayCommand CancelLoadingCommand { get; }
|
public IRelayCommand CancelLoadingCommand { get; }
|
||||||
|
public IRelayCommand DiagnoseProjectCommand { get; }
|
||||||
|
|
||||||
public MainViewModel()
|
public MainViewModel()
|
||||||
{
|
{
|
||||||
|
@ -125,6 +126,7 @@ namespace S7Explorer.ViewModels
|
||||||
ClearLogCommand = new RelayCommand(ClearLog);
|
ClearLogCommand = new RelayCommand(ClearLog);
|
||||||
RefreshProjectCommand = new RelayCommand<string>(RefreshProject); // Cambiado a string
|
RefreshProjectCommand = new RelayCommand<string>(RefreshProject); // Cambiado a string
|
||||||
CancelLoadingCommand = new RelayCommand(CancelLoading);
|
CancelLoadingCommand = new RelayCommand(CancelLoading);
|
||||||
|
DiagnoseProjectCommand = new RelayCommand(DiagnoseProject);
|
||||||
|
|
||||||
_parserService = new S7ParserService();
|
_parserService = new S7ParserService();
|
||||||
_parserService.LogEvent += OnParserLogEvent;
|
_parserService.LogEvent += OnParserLogEvent;
|
||||||
|
@ -134,6 +136,57 @@ namespace S7Explorer.ViewModels
|
||||||
LogText = "S7 Project Explorer iniciado. Versión 1.0\r\n";
|
LogText = "S7 Project Explorer iniciado. Versión 1.0\r\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DiagnoseProject()
|
||||||
|
{
|
||||||
|
if (_currentProject == null)
|
||||||
|
{
|
||||||
|
LogInfo("No hay proyecto para diagnosticar.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LogInfo("Iniciando diagnóstico del proyecto...");
|
||||||
|
|
||||||
|
// Explorar archivos principales
|
||||||
|
string projectDir = Path.GetDirectoryName(_currentProject.FilePath);
|
||||||
|
|
||||||
|
// S7RESOFF.DBF
|
||||||
|
string s7resoffPath = Path.Combine(projectDir, "hrs", "S7RESOFF.DBF");
|
||||||
|
if (File.Exists(s7resoffPath))
|
||||||
|
{
|
||||||
|
LogInfo("Diagnosticando S7RESOFF.DBF:");
|
||||||
|
DbfParser.LogColumnInfo(s7resoffPath, message => LogInfo(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
// SYMLISTS.DBF
|
||||||
|
string symlistsPath = Path.Combine(projectDir, "YDBs", "SYMLISTS.DBF");
|
||||||
|
if (File.Exists(symlistsPath))
|
||||||
|
{
|
||||||
|
LogInfo("Diagnosticando SYMLISTS.DBF:");
|
||||||
|
DbfParser.LogColumnInfo(symlistsPath, message => LogInfo(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buscar un SUBBLK.DBF
|
||||||
|
foreach (var dir in Directory.GetDirectories(Path.Combine(projectDir, "ombstx", "offline")))
|
||||||
|
{
|
||||||
|
string subblkPath = Path.Combine(dir, "SUBBLK.DBF");
|
||||||
|
if (File.Exists(subblkPath))
|
||||||
|
{
|
||||||
|
LogInfo($"Diagnosticando SUBBLK.DBF en {dir}:");
|
||||||
|
DbfParser.LogColumnInfo(subblkPath, message => LogInfo(message));
|
||||||
|
break; // Solo el primero para no saturar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LogInfo("Diagnóstico completado.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogError($"Error durante el diagnóstico: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void RefreshProject(string useCacheString)
|
private void RefreshProject(string useCacheString)
|
||||||
{
|
{
|
||||||
// Convertir el string a booleano
|
// Convertir el string a booleano
|
||||||
|
|
|
@ -45,6 +45,8 @@
|
||||||
<Button Grid.Column="4" Content="Buscar" Command="{Binding SearchCommand}" Padding="8,4" Margin="0,0,8,0" />
|
<Button Grid.Column="4" Content="Buscar" Command="{Binding SearchCommand}" Padding="8,4" Margin="0,0,8,0" />
|
||||||
|
|
||||||
<TextBlock Grid.Column="5" Text="{Binding ProjectInfo}" VerticalAlignment="Center" />
|
<TextBlock Grid.Column="5" Text="{Binding ProjectInfo}" VerticalAlignment="Center" />
|
||||||
|
<Button Grid.Column="6" Content="Diagnosticar" Command="{Binding DiagnoseProjectCommand}" Padding="8,4"
|
||||||
|
Margin="0,0,8,0" ToolTip="Explorar estructura de archivos DBF" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
|
@ -62,7 +64,7 @@
|
||||||
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
|
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<Image Source="{Binding IconSource}" Width="16" Height="16" Margin="0,0,4,0" />
|
<Image Source="{Binding IconSource}" Width="16" Height="16" Margin="0,0,4,0" />
|
||||||
<TextBlock Text="{Binding Name}" />
|
<TextBlock Text="{Binding DisplayName}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</HierarchicalDataTemplate>
|
</HierarchicalDataTemplate>
|
||||||
</TreeView.ItemTemplate>
|
</TreeView.ItemTemplate>
|
||||||
|
@ -79,6 +81,7 @@
|
||||||
<!-- Property Grid -->
|
<!-- Property Grid -->
|
||||||
<xctk:PropertyGrid Grid.Column="2" Margin="0,0,8,8" SelectedObject="{Binding SelectedObject}"
|
<xctk:PropertyGrid Grid.Column="2" Margin="0,0,8,8" SelectedObject="{Binding SelectedObject}"
|
||||||
AutoGenerateProperties="True" ShowSearchBox="True" ShowSortOptions="True" ShowTitle="True" />
|
AutoGenerateProperties="True" ShowSearchBox="True" ShowSortOptions="True" ShowTitle="True" />
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Log Section Splitter -->
|
<!-- Log Section Splitter -->
|
||||||
|
|