From ea96ba2bb962964fd2c8baeb8eba30cd018338bf Mon Sep 17 00:00:00 2001 From: Miguel Date: Mon, 24 Mar 2025 16:52:18 +0100 Subject: [PATCH] Segunda Version con FC, FB y OB --- LinqExtensions.cs | 69 ++++++ Models/S7Block.cs | 63 +++--- Models/S7Function.cs | 169 ++++++++++++++ Parsers/DbfParser.cs | 74 +++++- Parsers/FCParser.cs | 439 ++++++++++++++++++++++++++++++++++++ Parsers/S7ProjectParser.cs | 56 ++++- ViewModels/MainViewModel.cs | 83 ++++++- Views/MainWindow.xaml.cs | 2 + 8 files changed, 903 insertions(+), 52 deletions(-) create mode 100644 LinqExtensions.cs create mode 100644 Models/S7Function.cs create mode 100644 Parsers/FCParser.cs diff --git a/LinqExtensions.cs b/LinqExtensions.cs new file mode 100644 index 0000000..44f8906 --- /dev/null +++ b/LinqExtensions.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace S7Explorer +{ + /// + /// Extensiones LINQ para funcionalidades específicas de la aplicación + /// + public static class LinqExtensions + { + /// + /// Cuenta elementos en una colección que cumplen un predicado + /// + public static int Count(this IEnumerable source, Func predicate) + { + if (source == null) + return 0; + + return source.Where(predicate).Count(); + } + + /// + /// Verifica si una colección contiene al menos un elemento + /// + public static bool Any(this IEnumerable source) + { + if (source == null) + return false; + + return source.Any(item => true); + } + + /// + /// Verifica si una colección contiene al menos un elemento que cumpla un predicado + /// + public static bool Any(this IEnumerable source, Func predicate) + { + if (source == null) + return false; + + using (var enumerator = source.GetEnumerator()) + { + while (enumerator.MoveNext()) + { + if (predicate(enumerator.Current)) + return true; + } + } + + return false; + } + + /// + /// Selecciona elementos de una colección que cumplen un predicado + /// + public static IEnumerable Where(this IEnumerable source, Func predicate) + { + if (source == null) + yield break; + + foreach (var item in source) + { + if (predicate(item)) + yield return item; + } + } + } +} \ No newline at end of file diff --git a/Models/S7Block.cs b/Models/S7Block.cs index 0cda6a1..727fb3c 100644 --- a/Models/S7Block.cs +++ b/Models/S7Block.cs @@ -1,5 +1,8 @@ using System; +using System.Collections.Generic; using System.ComponentModel; +using System.Linq; +using Newtonsoft.Json; namespace S7Explorer.Models { @@ -10,6 +13,7 @@ namespace S7Explorer.Models private string _version; private DateTime? _modified; private int _size; + private string _language; [DisplayName("Autor")] public string AuthorName @@ -81,6 +85,20 @@ namespace S7Explorer.Models } } + [DisplayName("Lenguaje")] + public string Language + { + get => _language; + set + { + if (_language != value) + { + _language = value; + OnPropertyChanged(nameof(Language)); + } + } + } + public S7Block() { // Establece valores predeterminados específicos para bloques @@ -113,49 +131,20 @@ namespace S7Explorer.Models public class S7FunctionBlock : S7Block { - private string _language; - - [DisplayName("Lenguaje")] - public string Language - { - get => _language; - set - { - if (_language != value) - { - _language = value; - OnPropertyChanged(nameof(Language)); - } - } - } - public S7FunctionBlock() { ObjectType = S7ObjectType.FunctionBlock; } } - public class S7Function : S7Block + /// + /// Clase para representar un parámetro de función + /// + public class FunctionParameter { - private string _returnType; - - [DisplayName("Tipo de Retorno")] - public string ReturnType - { - get => _returnType; - set - { - if (_returnType != value) - { - _returnType = value; - OnPropertyChanged(nameof(ReturnType)); - } - } - } - - public S7Function() - { - ObjectType = S7ObjectType.Function; - } + public string Name { get; set; } + public string DataType { get; set; } + public string Direction { get; set; } // IN, OUT, IN/OUT + public string Description { get; set; } } } \ No newline at end of file diff --git a/Models/S7Function.cs b/Models/S7Function.cs new file mode 100644 index 0000000..32f4ee2 --- /dev/null +++ b/Models/S7Function.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using Newtonsoft.Json; +using S7Explorer.Parsers; + +namespace S7Explorer.Models +{ + public class S7Function : S7Block + { + private string _returnType; + private List _parameters; + private string _interface; + + [DisplayName("Tipo de Retorno")] + [Description("Tipo de dato que retorna la función")] + [Category("Interfaz")] + public string ReturnType + { + get => _returnType; + set + { + if (_returnType != value) + { + _returnType = value; + OnPropertyChanged(nameof(ReturnType)); + } + } + } + + [Browsable(false)] + [JsonIgnore] + public List Parameters + { + get => _parameters; + set + { + if (_parameters != value) + { + _parameters = value; + OnPropertyChanged(nameof(Parameters)); + + // Actualizar la interfaz formateada cuando se cambian los parámetros + UpdateFormattedInterface(); + } + } + } + + [DisplayName("Interfaz")] + [Description("Interfaz de la función con sus parámetros")] + [Category("Interfaz")] + [EditorAttribute(typeof(System.ComponentModel.Design.MultilineStringEditor), + typeof(System.Drawing.Design.UITypeEditor))] + public string Interface + { + get => _interface; + set + { + if (_interface != value) + { + _interface = value; + OnPropertyChanged(nameof(Interface)); + } + } + } + + // Propiedades adicionales específicas de FC + + [DisplayName("Parámetros de Entrada")] + [Description("Número de parámetros de entrada")] + [Category("Estadísticas")] + public int InputParameterCount => + Parameters?.Count(p => p.Direction == "IN") ?? 0; + + [DisplayName("Parámetros de Salida")] + [Description("Número de parámetros de salida")] + [Category("Estadísticas")] + public int OutputParameterCount => + Parameters?.Count(p => p.Direction == "OUT") ?? 0; + + [DisplayName("Parámetros IN/OUT")] + [Description("Número de parámetros de entrada/salida")] + [Category("Estadísticas")] + public int InOutParameterCount => + Parameters?.Count(p => p.Direction == "IN/OUT") ?? 0; + + [DisplayName("Total Parámetros")] + [Description("Número total de parámetros")] + [Category("Estadísticas")] + public int TotalParameterCount => + Parameters?.Count ?? 0; + + public S7Function() + { + ObjectType = S7ObjectType.Function; + Parameters = new List(); + } + + /// + /// Actualiza la representación formateada de la interfaz basada en los parámetros + /// + private void UpdateFormattedInterface() + { + if (Parameters == null || Parameters.Count == 0) + { + Interface = "// No hay parámetros definidos"; + return; + } + + var builder = new System.Text.StringBuilder(); + + // Agrupar por dirección + var inputParams = Parameters.Where(p => p.Direction == "IN").ToList(); + var outputParams = Parameters.Where(p => p.Direction == "OUT").ToList(); + var inOutParams = Parameters.Where(p => p.Direction == "IN/OUT").ToList(); + + // Añadir tipo de retorno + builder.AppendLine($"FUNCTION {Name} : {ReturnType ?? "VOID"}"); + builder.AppendLine(); + + // Añadir parámetros de entrada + if (inputParams.Any()) + { + builder.AppendLine("VAR_INPUT"); + foreach (var param in inputParams) + { + builder.Append($" {param.Name} : {param.DataType}"); + if (!string.IsNullOrEmpty(param.Description)) + builder.Append($"; // {param.Description}"); + builder.AppendLine(); + } + builder.AppendLine("END_VAR"); + builder.AppendLine(); + } + + // Añadir parámetros de salida + if (outputParams.Any()) + { + builder.AppendLine("VAR_OUTPUT"); + foreach (var param in outputParams) + { + builder.Append($" {param.Name} : {param.DataType}"); + if (!string.IsNullOrEmpty(param.Description)) + builder.Append($"; // {param.Description}"); + builder.AppendLine(); + } + builder.AppendLine("END_VAR"); + builder.AppendLine(); + } + + // Añadir parámetros de entrada/salida + if (inOutParams.Any()) + { + builder.AppendLine("VAR_IN_OUT"); + foreach (var param in inOutParams) + { + builder.Append($" {param.Name} : {param.DataType}"); + if (!string.IsNullOrEmpty(param.Description)) + builder.Append($"; // {param.Description}"); + builder.AppendLine(); + } + builder.AppendLine("END_VAR"); + } + + Interface = builder.ToString(); + } + } +} \ No newline at end of file diff --git a/Parsers/DbfParser.cs b/Parsers/DbfParser.cs index 3472b08..cf87de1 100644 --- a/Parsers/DbfParser.cs +++ b/Parsers/DbfParser.cs @@ -18,7 +18,7 @@ namespace S7Explorer.Parsers try { - // Abrir tabla DBF con codificación específica + // Abrir tabla DBF con codificación específica (importante para caracteres especiales en STEP7) using var stream = File.OpenRead(filePath); using var table = Table.Open(stream); @@ -33,14 +33,23 @@ namespace S7Explorer.Parsers { // Obtener valor y convertir a string si no es null var value = reader.GetValue(fieldName); - record[fieldName] = value?.ToString() ?? string.Empty; - // Manejamos específicamente bytes para campos como "MC5CODE" - // que pueden contener datos binarios codificados como CP1252 + // Manejar los diferentes tipos de datos if (value is byte[] byteValue) { + // Para campos de tipo binario como MC5CODE record[fieldName] = Encoding.GetEncoding(1252).GetString(byteValue); } + else if (value is DateTime dateValue) + { + // Mantener formato consistente para fechas + record[fieldName] = dateValue.ToString("yyyy-MM-dd HH:mm:ss"); + } + else + { + // Para todos los demás tipos + record[fieldName] = value?.ToString() ?? string.Empty; + } } result.Add(record); @@ -60,7 +69,21 @@ namespace S7Explorer.Parsers if (string.IsNullOrWhiteSpace(value)) return null; - if (int.TryParse(value, out int result)) + // Eliminar espacios y caracteres no numéricos iniciales + string trimmedValue = value.Trim(); + int startIndex = 0; + while (startIndex < trimmedValue.Length && !char.IsDigit(trimmedValue[startIndex])) + startIndex++; + + if (startIndex >= trimmedValue.Length) + return null; + + // Extraer solo los dígitos + string numericPart = new string(trimmedValue.Substring(startIndex) + .TakeWhile(char.IsDigit) + .ToArray()); + + if (int.TryParse(numericPart, out int result)) return result; return null; @@ -72,8 +95,45 @@ namespace S7Explorer.Parsers if (string.IsNullOrEmpty(input)) return string.Empty; - byte[] bytes = Encoding.GetEncoding(1252).GetBytes(input); - return Encoding.UTF8.GetString(bytes); + try + { + // Primero decodificar como Windows-1252 y luego encodear como UTF-8 + byte[] bytes = Encoding.GetEncoding(1252).GetBytes(input); + return Encoding.UTF8.GetString(bytes); + } + catch + { + // En caso de error, devolver el string original + return input; + } + } + + // Busca un archivo DBF en varias ubicaciones posibles basadas en el patrón de archivos STEP7 + public static string FindDbfFile(string basePath, string relativePath) + { + string path = Path.Combine(basePath, relativePath); + if (File.Exists(path)) + return path; + + // Comprobar si se puede encontrar en un directorio padre + string parentPath = Directory.GetParent(basePath)?.FullName; + if (!string.IsNullOrEmpty(parentPath)) + { + path = Path.Combine(parentPath, relativePath); + if (File.Exists(path)) + return path; + } + + // Intentar buscar por nombre de archivo en subdirectorios + string fileName = Path.GetFileName(relativePath); + foreach (var subdir in Directory.GetDirectories(basePath, "*", SearchOption.AllDirectories)) + { + path = Path.Combine(subdir, fileName); + if (File.Exists(path)) + return path; + } + + return null; } } } \ No newline at end of file diff --git a/Parsers/FCParser.cs b/Parsers/FCParser.cs new file mode 100644 index 0000000..2f97ae4 --- /dev/null +++ b/Parsers/FCParser.cs @@ -0,0 +1,439 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using S7Explorer.Models; + +namespace S7Explorer.Parsers +{ + /// + /// Parser específico para los bloques de función (FC) de STEP7 + /// + public class FCParser + { + private readonly string _projectDirectory; + + public FCParser(string projectDirectory) + { + _projectDirectory = projectDirectory; + } + + /// + /// Parsea los bloques FC desde los archivos SUBBLK.DBF del proyecto + /// + /// ID de la lista de subbloques del dispositivo + /// Lista de objetos S7Function representando los bloques FC + public List ParseFunctionBlocks(int subblockListId) + { + var functions = new List(); + + try + { + // Construir la ruta al archivo SUBBLK.DBF que contiene información de los bloques + string subblockFolder = $"{_projectDirectory}\\ombstx\\offline\\{subblockListId:X8}"; + string subblkPath = Path.Combine(subblockFolder, "SUBBLK.DBF"); + + if (!File.Exists(subblkPath)) + { + return CreateSampleFunctions(); // En caso de que no exista, usar datos de muestra + } + + // Leer datos del archivo DBF + var records = DbfParser.ReadDbfFile(subblkPath, new[] + { + "SUBBLKTYP", "BLKNUMBER", "BLKNAME", "AUTHOR", "FAMILY", + "VERSION", "CREATEDATE", "MODDATE", "INTERFLEN", "MC5LEN", "MC5CODE" + }); + + // Filtrar solo los registros que correspondan a FCs (tipo 00003) + var fcRecords = records.Where(r => r["SUBBLKTYP"] == "00003").ToList(); + + foreach (var record in fcRecords) + { + // Convertir número de bloque a entero + if (!int.TryParse(record["BLKNUMBER"], out int blockNumber)) + continue; + + // Crear objeto FC con los datos del registro + var fc = new S7Function + { + Number = $"FC{blockNumber}", + Name = DbfParser.ConvertCP1252ToUtf8(record["BLKNAME"]).Trim(), + AuthorName = DbfParser.ConvertCP1252ToUtf8(record["AUTHOR"]).Trim(), + Family = DbfParser.ConvertCP1252ToUtf8(record["FAMILY"]).Trim(), + Version = record["VERSION"].Trim(), + Size = ParseBlockSize(record["MC5LEN"]), + ObjectType = S7ObjectType.Function, + Language = DetermineLanguageFromMC5Code(record["MC5CODE"]), + ReturnType = DetermineReturnTypeFromMC5Code(record["MC5CODE"]) + }; + + // Intentar extraer fecha de modificación + if (DateTime.TryParse(record["MODDATE"], out DateTime modDate)) + { + fc.Modified = modDate; + } + + // Extraer más información del código MC5 + ExtractAdditionalInfoFromMC5Code(fc, record["MC5CODE"]); + + // Analizar variables de entrada/salida del interfaz + AnalyzeInterfaceData(fc, record["MC5CODE"]); + + functions.Add(fc); + } + + // Ordenar los bloques por número + functions = functions.OrderBy(f => ExtractNumber(f.Number)).ToList(); + } + catch (Exception ex) + { + // En caso de error, añadir un FC de error para informar al usuario + var errorFc = new S7Function + { + Name = "Error_Parsing_FC", + Number = "FC999", + Description = $"Error al parsear FCs: {ex.Message}", + ObjectType = S7ObjectType.Function + }; + functions.Add(errorFc); + } + + // Si no se encontraron FCs, usar datos de muestra + if (functions.Count == 0) + { + return CreateSampleFunctions(); + } + + return functions; + } + + /// + /// Extrae el número de un bloque a partir de su identificador (ej: "FC5" -> 5) + /// + private int ExtractNumber(string blockId) + { + if (string.IsNullOrEmpty(blockId) || blockId.Length < 3) + return 9999; // Valor alto para ordenar al final + + if (int.TryParse(blockId.Substring(2), out int number)) + return number; + + return 9999; + } + + /// + /// Parsea el tamaño del bloque a partir del valor MC5LEN + /// + private int ParseBlockSize(string mc5Len) + { + if (int.TryParse(mc5Len, out int size)) + return size; + + return 0; + } + + /// + /// Determina el lenguaje de programación a partir del código MC5 + /// + private string DetermineLanguageFromMC5Code(string mc5Code) + { + if (string.IsNullOrEmpty(mc5Code)) + return "Desconocido"; + + // Esta es una lógica simplificada. En una implementación real, + // necesitarías analizar patrones específicos en el código MC5 + if (mc5Code.Contains("STL") || mc5Code.Contains("AWL")) + return "AWL"; + else if (mc5Code.Contains("SCL")) + return "SCL"; + else if (mc5Code.Contains("GRAPH")) + return "GRAPH"; + else if (mc5Code.Contains("KOP") || mc5Code.Contains("LAD")) + return "KOP"; + else if (mc5Code.Contains("FUP") || mc5Code.Contains("FBD")) + return "FUP"; + + // Por defecto asumimos AWL (lenguaje más común) + return "AWL"; + } + + /// + /// Determina el tipo de retorno a partir del código MC5 + /// + private string DetermineReturnTypeFromMC5Code(string mc5Code) + { + if (string.IsNullOrEmpty(mc5Code)) + return "VOID"; + + // Esta es una lógica simplificada. En una implementación real, + // necesitarías analizar patrones específicos en el código MC5 + if (mc5Code.Contains("BOOL") && mc5Code.Contains("RET_VAL")) + return "BOOL"; + else if (mc5Code.Contains("INT") && mc5Code.Contains("RET_VAL")) + return "INT"; + else if (mc5Code.Contains("REAL") && mc5Code.Contains("RET_VAL")) + return "REAL"; + else if (mc5Code.Contains("WORD") && mc5Code.Contains("RET_VAL")) + return "WORD"; + else if (mc5Code.Contains("DWORD") && mc5Code.Contains("RET_VAL")) + return "DWORD"; + else if (mc5Code.Contains("TIME") && mc5Code.Contains("RET_VAL")) + return "TIME"; + + // Buscar cualquier tipo de dato asociado a RET_VAL + int retValPos = mc5Code.IndexOf("RET_VAL"); + if (retValPos > 0) + { + string[] dataTypes = { "BYTE", "DINT", "CHAR", "STRING", "DATE", "TIME_OF_DAY", "DATE_AND_TIME" }; + foreach (string type in dataTypes) + { + if (mc5Code.IndexOf(type, retValPos - 50, 100) > 0) + return type; + } + } + + return "VOID"; + } + + /// + /// Extrae información adicional del código MC5 + /// + private void ExtractAdditionalInfoFromMC5Code(S7Function fc, string mc5Code) + { + if (string.IsNullOrEmpty(mc5Code)) + return; + + try + { + // Extraer descripción (comentario del bloque) + int descStart = mc5Code.IndexOf("//"); + if (descStart >= 0) + { + int descEnd = mc5Code.IndexOf('\n', descStart); + if (descEnd > descStart) + { + string comment = mc5Code.Substring(descStart + 2, descEnd - descStart - 2).Trim(); + if (!string.IsNullOrEmpty(comment)) + fc.Description = DbfParser.ConvertCP1252ToUtf8(comment); + } + } + + // Añadir otros metadatos que se puedan extraer + } + catch + { + // Ignorar errores en la extracción para no detener el proceso + } + } + + /// + /// Analiza los datos de interfaz para extraer parámetros de entrada/salida + /// + private void AnalyzeInterfaceData(S7Function fc, string mc5Code) + { + if (string.IsNullOrEmpty(mc5Code)) + return; + + try + { + // Crear lista de parámetros + fc.Parameters = new List(); + + // Buscar sección de interfaz + int varInputPos = mc5Code.IndexOf("VAR_INPUT"); + int varOutputPos = mc5Code.IndexOf("VAR_OUTPUT"); + int varInOutPos = mc5Code.IndexOf("VAR_IN_OUT"); + + // Procesar parámetros de entrada + if (varInputPos >= 0) + { + int endPos = DetermineNextSectionPos(mc5Code, varInputPos); + string inputSection = mc5Code.Substring(varInputPos, endPos - varInputPos); + ExtractParameters(inputSection, "IN", fc.Parameters); + } + + // Procesar parámetros de salida + if (varOutputPos >= 0) + { + int endPos = DetermineNextSectionPos(mc5Code, varOutputPos); + string outputSection = mc5Code.Substring(varOutputPos, endPos - varOutputPos); + ExtractParameters(outputSection, "OUT", fc.Parameters); + } + + // Procesar parámetros de entrada/salida + if (varInOutPos >= 0) + { + int endPos = DetermineNextSectionPos(mc5Code, varInOutPos); + string inOutSection = mc5Code.Substring(varInOutPos, endPos - varInOutPos); + ExtractParameters(inOutSection, "IN/OUT", fc.Parameters); + } + } + catch + { + // Ignorar errores en la extracción para no detener el proceso + } + } + + /// + /// Determina la posición de la siguiente sección en el código + /// + private int DetermineNextSectionPos(string code, int startPos) + { + string[] sections = { "VAR_INPUT", "VAR_OUTPUT", "VAR_IN_OUT", "VAR_TEMP", "VAR", "BEGIN" }; + + int minPos = code.Length; + foreach (string section in sections) + { + int pos = code.IndexOf(section, startPos + 1); + if (pos > 0 && pos < minPos) + minPos = pos; + } + + return minPos; + } + + /// + /// Extrae parámetros individuales de una sección de interfaz + /// + private void ExtractParameters(string section, string direction, List parameters) + { + // Dividir por líneas + string[] lines = section.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + + foreach (string line in lines) + { + string trimmedLine = line.Trim(); + + // Ignorar líneas de declaración de sección, comentarios o END_VAR + if (trimmedLine.StartsWith("VAR_") || trimmedLine.StartsWith("//") || + trimmedLine.StartsWith("END_VAR") || string.IsNullOrWhiteSpace(trimmedLine)) + continue; + + // Procesar línea con declaración de parámetro + int colonPos = trimmedLine.IndexOf(':'); + if (colonPos > 0) + { + string paramName = trimmedLine.Substring(0, colonPos).Trim(); + + // Extraer tipo y comentario + string remainder = trimmedLine.Substring(colonPos + 1).Trim(); + string paramType = remainder; + string comment = string.Empty; + + // Verificar si hay comentario en la línea + int commentPos = remainder.IndexOf("//"); + if (commentPos > 0) + { + paramType = remainder.Substring(0, commentPos).Trim(); + comment = remainder.Substring(commentPos + 2).Trim(); + } + + // Limpiar cualquier punto y coma al final del tipo + if (paramType.EndsWith(";")) + paramType = paramType.Substring(0, paramType.Length - 1).Trim(); + + // Añadir parámetro a la lista + parameters.Add(new FunctionParameter + { + Name = paramName, + DataType = paramType, + Direction = direction, + Description = comment + }); + } + } + } + + /// + /// Crea bloques FC de ejemplo cuando no se pueden parsear los reales + /// + private List CreateSampleFunctions() + { + return new List + { + new S7Function { + Name = "Calc_Setpoint", + Number = "FC1", + Size = 124, + ReturnType = "REAL", + Description = "Cálculo de punto de consigna", + Modified = DateTime.Now.AddDays(-8), + Language = "SCL", + Parameters = new List + { + new FunctionParameter { Name = "Target", DataType = "REAL", Direction = "IN", Description = "Valor objetivo" }, + new FunctionParameter { Name = "Actual", DataType = "REAL", Direction = "IN", Description = "Valor actual" }, + new FunctionParameter { Name = "Gain", DataType = "REAL", Direction = "IN", Description = "Ganancia" } + } + }, + new S7Function { + Name = "Scale_Analog", + Number = "FC2", + Size = 68, + ReturnType = "REAL", + Description = "Escalado de valor analógico", + Modified = DateTime.Now.AddDays(-12), + Language = "AWL", + Parameters = new List + { + new FunctionParameter { Name = "Raw", DataType = "INT", Direction = "IN", Description = "Valor bruto" }, + new FunctionParameter { Name = "RawLow", DataType = "INT", Direction = "IN", Description = "Valor mínimo bruto" }, + new FunctionParameter { Name = "RawHigh", DataType = "INT", Direction = "IN", Description = "Valor máximo bruto" }, + new FunctionParameter { Name = "ScaleLow", DataType = "REAL", Direction = "IN", Description = "Valor mínimo escalado" }, + new FunctionParameter { Name = "ScaleHigh", DataType = "REAL", Direction = "IN", Description = "Valor máximo escalado" } + } + }, + new S7Function { + Name = "CheckAlarms", + Number = "FC3", + Size = 154, + ReturnType = "BOOL", + Description = "Verificación de alarmas", + Modified = DateTime.Now.AddDays(-5), + Language = "KOP", + Parameters = new List + { + new FunctionParameter { Name = "Value", DataType = "REAL", Direction = "IN", Description = "Valor a comprobar" }, + new FunctionParameter { Name = "LowLimit", DataType = "REAL", Direction = "IN", Description = "Límite inferior" }, + new FunctionParameter { Name = "HighLimit", DataType = "REAL", Direction = "IN", Description = "Límite superior" }, + new FunctionParameter { Name = "AlarmStatus", DataType = "WORD", Direction = "OUT", Description = "Estado de alarmas" } + } + }, + new S7Function { + Name = "Timer_Control", + Number = "FC4", + Size = 86, + ReturnType = "VOID", + Description = "Control de temporizadores", + Modified = DateTime.Now.AddDays(-3), + Language = "FUP", + Parameters = new List + { + new FunctionParameter { Name = "Start", DataType = "BOOL", Direction = "IN", Description = "Iniciar temporizador" }, + new FunctionParameter { Name = "Duration", DataType = "TIME", Direction = "IN", Description = "Duración" }, + new FunctionParameter { Name = "TimerNo", DataType = "INT", Direction = "IN", Description = "Número de temporizador" }, + new FunctionParameter { Name = "Status", DataType = "BOOL", Direction = "OUT", Description = "Estado (activo/inactivo)" }, + new FunctionParameter { Name = "ElapsedTime", DataType = "TIME", Direction = "OUT", Description = "Tiempo transcurrido" } + } + }, + new S7Function { + Name = "String_Process", + Number = "FC5", + Size = 210, + ReturnType = "INT", + Description = "Procesamiento de cadenas", + Modified = DateTime.Now.AddDays(-1), + Language = "SCL", + Parameters = new List + { + new FunctionParameter { Name = "InStr", DataType = "STRING", Direction = "IN", Description = "Cadena de entrada" }, + new FunctionParameter { Name = "Operation", DataType = "INT", Direction = "IN", Description = "Operación a realizar" }, + new FunctionParameter { Name = "OutStr", DataType = "STRING", Direction = "OUT", Description = "Cadena de salida" } + } + } + }; + } + } +} \ No newline at end of file diff --git a/Parsers/S7ProjectParser.cs b/Parsers/S7ProjectParser.cs index ef598b5..062af74 100644 --- a/Parsers/S7ProjectParser.cs +++ b/Parsers/S7ProjectParser.cs @@ -225,14 +225,56 @@ namespace S7Explorer.Parsers private void ParseBlocks(S7Object dbFolder, S7Object fbFolder, S7Object fcFolder, S7Object obFolder, int subblockListId) { - // Esta es una implementación de muestra - // En una versión completa, leerías los archivos SUBBLK.DBF reales + try + { + // Usar parser específico para las funciones (FC) + var fcParser = new FCParser(_projectDirectory); + var functions = fcParser.ParseFunctionBlocks(subblockListId); - // Crear algunos bloques de ejemplo - AddSampleDataBlocks(dbFolder); - AddSampleFunctionBlocks(fbFolder); - AddSampleFunctions(fcFolder); - AddSampleOrgBlocks(obFolder); + // Añadir bloques FC al árbol de proyecto + foreach (var fc in functions) + { + fc.Parent = fcFolder; + fcFolder.Children.Add(fc); + } + + // Para los otros tipos de bloques, seguimos usando los ejemplos por ahora + // En una implementación completa, crearíamos parsers específicos para cada tipo + AddSampleDataBlocks(dbFolder); + AddSampleFunctionBlocks(fbFolder); + AddSampleOrgBlocks(obFolder); + } + catch (Exception ex) + { + // En caso de error, añadimos un objeto de error informativo + var errorObject = new S7Object + { + Name = "Error en parseo de bloques", + Description = ex.Message, + ObjectType = S7ObjectType.Folder + }; + + // Lo añadimos a cada carpeta para que sea visible + dbFolder.Children.Add(errorObject); + fbFolder.Children.Add(new S7Object + { + Name = errorObject.Name, + Description = errorObject.Description, + ObjectType = errorObject.ObjectType + }); + fcFolder.Children.Add(new S7Object + { + Name = errorObject.Name, + Description = errorObject.Description, + ObjectType = errorObject.ObjectType + }); + obFolder.Children.Add(new S7Object + { + Name = errorObject.Name, + Description = errorObject.Description, + ObjectType = errorObject.ObjectType + }); + } } private void AddSampleDataBlocks(S7Object dbFolder) diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs index aeb6e00..f01a2f4 100644 --- a/ViewModels/MainViewModel.cs +++ b/ViewModels/MainViewModel.cs @@ -46,10 +46,38 @@ namespace S7Explorer.ViewModels { // Actualizar el objeto seleccionado para PropertyGrid SelectedObject = _selectedTreeItem; + + // Acciones específicas según el tipo de objeto seleccionado + if (_selectedTreeItem is S7Function fc) + { + // Si el objeto seleccionado es una función, actualizar su interfaz + UpdateFunctionInterface(fc); + } } } } + /// + /// Actualiza la información de interfaz de una función seleccionada + /// + private void UpdateFunctionInterface(S7Function fc) + { + try + { + // Aquí podríamos cargar información adicional específica de la función + // Por ejemplo, cargar el código de la función, detalles de parámetros, etc. + + // Actualizar estadísticas o análisis de la función + // Por ejemplo, mostrar uso de la función en otros bloques + + // Este método se ejecutará cuando el usuario seleccione una FC en el árbol + } + catch (Exception) + { + // Ignorar errores para no interrumpir la experiencia del usuario + } + } + public ObservableCollection ProjectStructure { get => _projectStructure; @@ -138,8 +166,61 @@ namespace S7Explorer.ViewModels // Recopilar todos los objetos en el proyecto var allObjects = GetAllObjects(_currentProject); + // Lista para almacenar coincidencias + var matchingObjects = new List(); + + // Buscar en función del texto de búsqueda + string searchText = SearchText.Trim().ToLowerInvariant(); + // Buscar objetos que coincidan con el texto - var matchingObjects = allObjects.Where(o => o.ContainsText(SearchText)).ToList(); + foreach (var obj in allObjects) + { + // Comprobar si el texto de búsqueda parece ser una referencia a FC específica + if (searchText.StartsWith("fc") && searchText.Length >= 3) + { + // Si es una función específica (por ejemplo "fc5") + if (obj is S7Function func && + func.Number.ToLowerInvariant() == searchText) + { + matchingObjects.Add(func); + } + } + // Comprobar si queremos encontrar todas las FCs + else if (searchText == "fc" || searchText == "función" || searchText == "funcion") + { + if (obj is S7Function) + { + matchingObjects.Add(obj); + } + } + // Búsqueda en parámetros de función + else if (obj is S7Function func) + { + if (func.ContainsText(searchText)) + { + matchingObjects.Add(func); + } + else if (func.Parameters != null) + { + // Buscar en los parámetros de la función + foreach (var param in func.Parameters) + { + if (param.Name.ToLowerInvariant().Contains(searchText) || + param.DataType.ToLowerInvariant().Contains(searchText) || + (param.Description?.ToLowerInvariant().Contains(searchText) ?? false)) + { + matchingObjects.Add(func); + break; + } + } + } + } + // Búsqueda general para otros objetos + else if (obj.ContainsText(searchText)) + { + matchingObjects.Add(obj); + } + } if (matchingObjects.Count == 0) { diff --git a/Views/MainWindow.xaml.cs b/Views/MainWindow.xaml.cs index fbc3f4d..271a412 100644 --- a/Views/MainWindow.xaml.cs +++ b/Views/MainWindow.xaml.cs @@ -23,6 +23,8 @@ namespace S7Explorer.Views } } + + private void SearchBox_KeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Enter && ViewModel != null)