SerialPort en .NET: Gestión de Búferes y Ejemplo de Parsing

En la implementación de SerialPort en .NET, el manejo de datos involucra dos capas de búfer que afectan el flujo de información. Comprender este comportamiento es esencial para evitar datos residuales no deseados.

Capas de Búfer en SerialPort

Los búferes se dividen en dos niveles principales:

  • Búfer del controlador (kernel): Ubicado dentro del controlador del sistema operativo, persiste mientras el dispositivo esté físicamente cnoectado. Se descarta al desconectar el cable, reiniciar el controlador o al leer/desechar los datos.
  • Búfer de entrada de .NET: Administrado por el objeto SerialPort, su tamaño se define por ReadBufferSize (predeterminado: 4 KB). Solo está activo cuando el puerto está abierto; al llamar a Close(), se libera y descarta.

Comportamiento al Cerrar y Abrir el Puerto

Al invocar Close(), .NET cierra el identificador subyacente del puerto serie y descarta ambos búferes (tanto de entrada como de salida). El controlador del sistema operativo también libera el búfer de datos no leídos. Por lo tanto, una vez cerrado el puerto, no es posible recuperar datos antiguos almacenados.

Al reabrir el puerto con Open(), los búferes se inicializan como vacíos. No se conserva información de sesiones anteriores, a menos que el dispositivo transmita nuevos datos inmediatamente después de la apertura.

Si se observan datos "antiguos" tras abrir el puerto, generalmente se debe a que el dispositivo envió información durante el intervalo entre cierre y apertura, o no se descartaron búferes residuales. Para mitigar esto, se recomienda:

puerto.Open();
puerto.DiscardInBuffer();
puerto.DiscardOutBuffer();

Esto garantiza un estado inicial limpio.

Retención de Búferes

En la capa del controlador, los datos se sobrescriben en caso de desbordamiento. En .NET, los búferes retienen información mientras el puerto esté abierto, hasta que se leen o se descartan explícitamente. El cierre del puerto provoca el descarte inmediato de todos los datos pendientes.

Ejemplo de Parsing Asíncrono para Paquetes Serie

A continuación, se presenta un ejemplo optimizado para analizar paquetes de alta velocidad, con soporte para flujo de control y gestión asíncrona. El código renombra variables y reestructura lógica para mayor claridad, manteniendo la funcionalidad original.

using System;
using System.Collections.Concurrent;
using System.IO.Ports;
using System.Threading;
using System.Threading.Tasks;

namespace UtilidadesSerie
{
    public class AnalizadorPaquetes : IDisposable
    {
        private readonly SerialPort _puerto;
        private readonly byte[] _comando;
        private readonly CancellationTokenSource _tokenCancelar = new CancellationTokenSource();
        private readonly ConcurrentQueue<byte> _datosSinProcesar = new ConcurrentQueue<byte>();

        private static readonly byte[] MarcaInicio = { 0xAA, 0xBB, 0xCC, 0xDD };
        private static readonly byte[] MarcaFin = { 0xAA, 0xBB, 0xCC, 0xFF };

        public event Action<ushort> PaqueteRecibido;

        private bool _paqueteProcesado = false;

        public AnalizadorPaquetes(SerialPort puerto, byte[] comando)
        {
            _puerto = puerto ?? throw new ArgumentNullException(nameof(puerto));
            _comando = comando ?? throw new ArgumentNullException(nameof(comando));

            _puerto.ReadBufferSize = 65536;
            _puerto.WriteBufferSize = 65536;
            _puerto.ReceivedBytesThreshold = 1;
            _puerto.Handshake = Handshake.RequestToSend;

            _puerto.Open();
            _puerto.DiscardInBuffer();
            _puerto.DiscardOutBuffer();

            _puerto.DataReceived += AlRecibirDatos;
            _puerto.Write(_comando, 0, _comando.Length);

            Task.Run(() => BucleProcesamiento(_tokenCancelar.Token));
        }

        private void AlRecibirDatos(object sender, SerialDataReceivedEventArgs e)
        {
            if (_paqueteProcesado) return;
            int bytesDisponibles = _puerto.BytesToRead;
            if (bytesDisponibles <= 0) return;

            var bufferTemporal = new byte[bytesDisponibles];
            _puerto.Read(bufferTemporal, 0, bytesDisponibles);
            _datosSinProcesar.Enqueue(bufferTemporal);
        }

        private async Task BucleProcesamiento(CancellationToken token)
        {
            byte[] acumulador = Array.Empty<byte>();
            while (!token.IsCancellationRequested && !_paqueteProcesado)
            {
                if (_datosSinProcesar.IsEmpty)
                {
                    await Task.Delay(1, token).ConfigureAwait(false);
                    continue;
                }

                var fragmentos = new System.Collections.Generic.List<byte[]>();
                while (_datosSinProcesar.TryDequeue(out var fragmento))
                    fragmentos.Add(fragmento);

                int totalBytes = 0;
                foreach (var frag in fragmentos)
                    totalBytes += frag.Length;
                var bufferCombinado = new byte[acumulador.Length + totalBytes];
                Buffer.BlockCopy(acumulador, 0, bufferCombinado, 0, acumulador.Length);
                int desplazamiento = acumulador.Length;
                foreach (var frag in fragmentos)
                {
                    Buffer.BlockCopy(frag, 0, bufferCombinado, desplazamiento, frag.Length);
                    desplazamiento += frag.Length;
                }
                acumulador = bufferCombinado;

                int posicionActual = 0;
                while (!_paqueteProcesado)
                {
                    int inicio = BuscarIndice(acumulador, MarcaInicio, posicionActual);
                    if (inicio < 0) break;
                    int fin = BuscarIndice(acumulador, MarcaFin, inicio + MarcaInicio.Length);
                    if (fin < 0) break;

                    int longitudPaquete = fin + MarcaFin.Length - inicio;
                    var paqueteCompleto = new byte[longitudPaquete];
                    Buffer.BlockCopy(acumulador, inicio, paqueteCompleto, 0, longitudPaquete);

                    int longitudCarga = longitudPaquete - MarcaInicio.Length - MarcaFin.Length;
                    var cargaUtil = new byte[longitudCarga];
                    Buffer.BlockCopy(paqueteCompleto, MarcaInicio.Length, cargaUtil, 0, longitudCarga);
                    var datosConvertidos = new ushort[longitudCarga / 2];
                    for (int i = 0; i < datosConvertidos.Length; i++)
                        datosConvertidos[i] = (ushort)(cargaUtil[2 * i] | (cargaUtil[2 * i + 1] << 8));

                    _paqueteProcesado = true;
                    PaqueteRecibido?.Invoke(datosConvertidos);
                    Dispose();
                    break;
                }

                if (!_paqueteProcesado && posicionActual < acumulador.Length)
                {
                    var datosRestantes = new byte[acumulador.Length - posicionActual];
                    Buffer.BlockCopy(acumulador, posicionActual, datosRestantes, 0, datosRestantes.Length);
                    acumulador = datosRestantes;
                }
                else if (!_paqueteProcesado)
                {
                    acumulador = Array.Empty<byte>();
                }
            }
        }

        private int BuscarIndice(byte[] datos, byte[] patron, int inicioBusqueda)
        {
            for (int i = inicioBusqueda; i <= datos.Length - patron.Length; i++)
            {
                bool coincide = true;
                for (int j = 0; j < patron.Length; j++)
                    if (datos[i + j] != patron[j]) { coincide = false; break; }
                if (coincide) return i;
            }
            return -1;
        }

        public void Dispose()
        {
            _tokenCancelar.Cancel();
            _puerto.DataReceived -= AlRecibirDatos;
            try { if (_puerto.IsOpen) _puerto.Close(); } catch { }
            try { _puerto.Dispose(); } catch { }
        }
    }
}
</ushort></byte></byte>

Este enfoque asegura que solo se procese el primer paquete completo, optimizando recrusos en aplicaciones de alto rendimiento.

Etiquetas: .NET SerialPort CSharp buffers asynchronous-programming

Publicado el 5-30 05:34