Implementación del protocolo OMRON FINS-TCP en C# para comunicación con PLC

  1. Diseño de la arquitectura de comunicación

1.1 Arquitectura en capas del protocolo

public class ClienteComunicacionOmronFins
{
    // Capa de transporte
    private TcpClient _socketTcp;
    private NetworkStream _flujoRed;
    
    // Capa de protocolo
    private byte[] _cabecera = { 0x46, 0x49, 0x4E, 0x53 }; // Cabecera FINS
    private byte[] _paqueteMano = new byte[20];           // Paquete de handshake
    
    // Gestión de estado
    private bool _conectado = false;
    private byte _nodoCliente;                           // Nodo del cliente
    private byte _nodoServidor;                          // Nodo del PLC
}

  1. Implementación central

2.1 Conexión TCP y establecimiento de handshake

public bool Conectar(string direccionIp, int puerto = 9600)
{
    try
    {
        _socketTcp = new TcpClient();
        _socketTcp.Connect(IPAddress.Parse(direccionIp), puerto);
        _flujoRed = _socketTcp.GetStream();
        
        // Construir paquete de handshake
        PrepararPaqueteMano();
        _flujoRed.Write(_paqueteMano, 0, _paqueteMano.Length);
        
        // Recibir respuesta
        byte[] buffer = new byte[24];
        int bytesLeidos = _flujoRed.Read(buffer, 0, buffer.Length);
        
        if (buffer[0] == 0x46 && buffer[1] == 0x49 && buffer[2] == 0x4E && buffer[3] == 0x53)
        {
            _nodoServidor = buffer[23]; // Obtener nodo del PLC
            _conectado = true;
            return true;
        }
    }
    catch (Exception ex)
    {
        Debug.WriteLine($"Error de conexión: {ex.Message}");
    }
    return false;
}

private void PrepararPaqueteMano()
{
    Array.Clear(_paqueteMano, 0, _paqueteMano.Length);
    _paqueteMano[0] = 0x46; // F
    _paqueteMano[1] = 0x49; // I
    _paqueteMano[2] = 0x4E; // N
    _paqueteMano[3] = 0x53; // S
    _paqueteMano[7] = 0x0C; // Longitud de datos
    _paqueteMano[16] = 0x00; // Nodo del cliente (bajo)
    _paqueteMano[17] = 0x00; // Nodo del cliente (alto)
}

2.2 Operaciones de lectura y escritura de datos

/// <summary>
/// Leer registros de datos del PLC (área D)
/// </summary>
public ushort[] LeerRegistrosD(string direccion, int cantidad)
{
    ushort inicio = Convert.ToUInt16(direccion.Substring(1));
    byte area = ObtenerCodigoArea(direccion);
    
    // Construir trama de comando
    byte[] comando = ConstruirComando(
        ComandoFins.LecturaAreaMemoria,
        area,
        inicio,
        cantidad
    );
    
    // Enviar y recibir datos
    EnviarComando(comando);
    return ProcesarRespuesta(cantidad * 2);
}

/// <summary>
/// Escribir en registros de retención del PLC (área HR)
/// </summary>
public bool EscribirRegistrosHR(string direccion, ushort[] valores)
{
    ushort inicio = Convert.ToUInt16(direccion.Substring(1));
    byte area = ObtenerCodigoArea(direccion);
    
    // Construir trama de comando
    byte[] comando = ConstruirComando(
        ComandoFins.EscrituraAreaMemoria,
        area,
        inicio,
        valores
    );
    
    // Enviar y verificar
    EnviarComando(comando);
    return VerificarConfirmacion();
}

private byte[] ConstruirComando(byte codigoPrincipal, byte codigoSecundario, ushort direccion, ushort cantidad)
{
    byte[] cmd = new byte[26];
    cmd[6] = 0x1A; // Longitud de datos
    cmd[7] = 0x02; // Tipo de comando
    cmd[20] = codigoPrincipal; // Código de solicitud principal
    cmd[21] = codigoSecundario; // Código de solicitud secundario
    cmd[22] = area; // Código de área
    // Codificación de dirección y longitud...
    return cmd;
}

  1. Implementación de funcionalidades clave

3.1 Mapeo de códigos de área

private byte ObtenerCodigoArea(string area)
{
    return area switch
    {
        "DM" => 0x82,
        "HR" => 0xB2,
        "WR" => 0x31,
        "CIO" => 0x30,
        _ => throw new ArgumentException("Código de área no válido")
    };
}

3.2 Análisis de mensajes

private ushort[] ProcesarRespuesta(int longitudEsperada)
{
    byte[] buffer = new byte[longitudEsperada];
    int bytesLeidos = _flujoRed.Read(buffer, 0, buffer.Length);
    
    if (buffer[12] != 0x00 || buffer[13] != 0x00) // Verificar código de error
        throw new Exception("El PLC devolvió un código de error");
    
    // Convertir a arreglo de ushort
    ushort[] resultado = new ushort[bytesLeidos / 2];
    for (int i = 0; i < resultado.Length; i++)
    {
        resultado[i] = (ushort)(buffer[2*i] << 8 | buffer[2*i+1]);
    }
    return resultado;
}

  1. Manejo de excepciones y optimización

4.1 Mecanismo de reconexión automática

public void Reconectar()
{
    Desconectar();
    int intentos = 0;
    while (!_conectado && intentos < 3)
    {
        try
        {
            Conectar(DireccionIp, Puerto);
            intentos++;
        }
        catch { Thread.Sleep(500); }
    }
}

4.2 Optimización de rendimiento

// Optimización de operaciones por lotes
public void EscrituraPorLotes(Dictionary<string, ushort[]> datos)
{
    using (var flujoMemoria = new MemoryStream())
    {
        foreach (var elemento in datos)
        {
            byte[] cmd = ConstruirComando(
                ComandoFins.EscrituraAreaMemoria,
                ObtenerCodigoArea(elemento.Key),
                Convert.ToUInt16(elemento.Key.Substring(1)),
                elemento.Value
            );
            flujoMemoria.Write(cmd, 0, cmd.Length);
        }
        _flujoRed.Write(flujoMemoria.ToArray(), 0, flujoMemoria.Length);
    }
}

  1. Ejemplo completo de uso

var plc = new ClienteComunicacionOmronFins();
if(plc.Conectar("192.168.1.100"))
{
    try
    {
        // Leer D100-D105
        ushort[] datos = plc.LeerRegistrosD("D100", 6);
        plc.EscribirRegistrosHR("HR10", new ushort[] { 1234, 5678 });
        
        // Monitoreo en tiempo real
        Timer temporizador = new Timer(1000);
        temporizador.Elapsed += (s,e) => 
        {
            float temperatura = plc.LeerFlotante("DM100");
            Console.WriteLine($"Temperatura: {temperatura}℃");
        };
        temporizador.Start();
    }
    finally
    {
        plc.Desconectar();
    }
}

  1. Herramientas de depuración y verificación

1. Análisis de paquetes con Wireshark:
Filtrar tcp.port==9600 para inspeccionar la estructura de mensajes FINS y validar el handshake y tramas de datos.

2. Herramienta de monitoreo de PLC:
Utilizar Omron FinsTool para verificar la comunicación básica, comparando resultados manuales con la ejecución del programa.

3. Registro de logs:

_flujoRed.Write(Encoding.ASCII.GetBytes($"[{DateTime.Now}] Escrito en HR10: 1234\r\n"));

  1. Solución a problemas comunes

Fenómeno del problema Solución
Tiempo de conexión agotado Veriifcar configuración de firewall y estado de conexión del puerto del PLC
Error de validación de datos Asegurar la conversión correcta del orden de bytes (modo big-endian)
Fallo en escritura por lotes Dividir operaciones (≤2000 bytes por vez) y añadir mecanismo de reintentos
Rendimiento insuficiente en tiempo real Habilitar comunicación asíncrona y establecer _socketTcp.ReceiveTimeout = 1000;
  1. Implementación de funciones extendidas

8.1 Mecanismo de caché de datos

private Dictionary<string, ushort[]> _almacenCache = new();
public ushort[] LeerConCache(string direccion, int cantidad)
{
    if (!_almacenCache.ContainsKey(direccion) || _almacenCache[direccion] == null)
    {
        _almacenCache[direccion] = LeerRegistrosD(direccion, cantidad);
    }
    return _almacenCache[direccion];
}

8.2 Manejo de alarmas

public void VerificarAlarmaTemperatura(string direccion, float umbral)
{
    float temp = LeerFlotante(direccion);
    if (temp > umbral)
    {
        EnviarAlertaEmail($"¡Alarma de temperatura! {direccion}: {temp}℃");
        plc.EscribirBit("CIO.0.0", true); // Activar relé de alarma
    }
}

  1. Recomendaciones de despliegue de ingeniería

1. Configuración de hardware:

  • PC industrial: Computadora industrial Advantech (soporte para operación en amplio rango de temperatura)
  • Equipos de red: PLC OMRON CP1E + Switch compatible con FINS

2. Configuración de seguridad:

// Habilitar comunicación cifrada
_flujoRed = new SslStream(_socketTcp.GetStream(), false);
_flujoRed.AuthenticateAsClient("PLC001");

Etiquetas: CSharp OMRON FINS-TCP PLC tcp

Publicado el 5-31 03:06