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