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" } } } }; } } }