在c#中解码原始串行协议

时间:2018-01-08 20:23:45

标签: c# serial-port protocols binary-data

我在80年代与C一起长大,但是在c#和面向对象编程方面是一个完全的初学者,所以你们中的一些人可能对此很开心。

我正在尝试编写一个简单的应用程序来解码来自实验室仪器的遥测数据。我计划使用Windows Forms编写最终代码以获得外观和感觉,但我将核心机制的实验性实现作为控制台应用程序。

数据流包含以0x55 0x2f 0x48开头的72字节消息,然后包含有效负载数据并以校验和结束。我想“捕获”这些消息,丢弃那些校验和错误并解码部分有效负载以供显示。

测试应用程序运行良好,但只是打印传入的数据。 (虽然由于其原始设计,当然在有效载荷中将0x55作为新消息的开始)

这里最好的做法是什么?我应该将状态机放入串口的事件处理程序中,还是有一个定期检查输入缓冲区的定时器?我实际上甚至不确定如何访问事件处理程序“外部”的传入数据。任何帮助或指针将不胜感激。

我的实验应用程序的代码是:

using System;
using System.IO.Ports;

namespace Serial_port_experiment_console 
{
    class Program 
    {
        static void Main(string[] args) 
        {
            var enable_telemetry = new byte[] { 0x55, 0x92, 4, 0x15 };
            var disable_telemetry = new byte[] { 0x55, 0x91, 4, 0x16 };

            SerialPort sp = new SerialPort("COM12", 9600, Parity.None, 8, StopBits.One);
            sp.Open();

            sp.Write(enable_telemetry, 0, enable_telemetry.Length); // Start telemetry

            sp.DataReceived += port_OnReceiveData;

            Console.ReadLine(); // Wait for keyboard RETURN before closing program

            sp.Write(disable_telemetry, 0, disable_telemetry.Length); // Stop telemetry

            sp.Close();
        }

        private static void port_OnReceiveData(object sender, SerialDataReceivedEventArgs e) 
        {
            SerialPort spL = (SerialPort)sender;
            byte[] buf = new byte[spL.BytesToRead];
            spL.Read(buf, 0, buf.Length);
            foreach (Byte b in buf) 
            {
                if (b == 0x55) { Console.WriteLine(); } // Start of a new report
                Console.Write(b.ToString("X")); // Write the hex code
            }
        }
    }
}

3 个答案:

答案 0 :(得分:1)

一些封装可以帮助你 - 我将读取的逻辑从端口与实际的行处理分成几个类。像这样:

static void Main(string[] args)
{
    Console.WriteLine("Starting reading data");
    var reader = new LinesReader();
    Console.WriteLine("Reading data...");

    try
    {
        reader.Start();

        Console.ReadKey();
    }
    finally
    {
        reader.Stop();
    }

    foreach (var line in reader.ValidLines)
    {
        line.DoSomethingWithBytes();
    }

    Console.ReadKey();
}

internal sealed class LinesReader
{
    private static readonly byte[] EnableCode = new byte[] { 0x55, 0x92, 4, 0x15 };
    private static readonly byte[] DisableCode = new byte[] { 0x55, 0x91, 4, 0x16 };
    private readonly SerialPort sp;
    private readonly List<LineInput> lines = new List<LineInput>();

    public LinesReader()
    {
        sp = new SerialPort("COM12", 9600, Parity.None, 8, StopBits.One);
        lines.Add(new LineInput());
    }

    public void Start()
    {
        sp.DataReceived += DataReceived;
        sp.Open();

        // Start telemetry
        sp.Write(EnableCode, 0, EnableCode.Length);
    }

    private void DataReceived(object sender, SerialDataReceivedEventArgs e)
    {
        LineInput current;
        byte[] buf = new byte[sp.BytesToRead];
        sp.Read(buf, 0, buf.Length);

        for (int offset = 0; offset < buf.Length; )
        {
            current = GetCurrentLine();
            offset = current.AddBytes(offset, buf);
        }
    }

    private LineInput GetCurrentLine()
    {
        if (lines.Count == 0 || lines[lines.Count - 1].IsComplete)
        {
            var ret = new LineInput();
            lines.Add(ret);
            Console.WriteLine($"Starting line {lines.Count}");
            return ret;
        }
        return lines[lines.Count - 1];
    }

    public IEnumerable<LineInput> ValidLines => lines.Where(e => e.IsValid);

    public void Stop()
    {
        // Stop telemetry
        sp.Write(DisableCode, 0, DisableCode.Length);

        sp.DataReceived -= DataReceived;

        sp.Close();
    }
}

internal sealed class LineInput
{
    private readonly List<byte> bytesInLine = new List<byte>();
    public static byte StartCode { get; } = 0x55;

    public bool IsComplete { get; private set; }

    public void DoSomethingWithBytes()
    {
        //Here I'm just printing the line.
        Console.WriteLine(bytesInLine);
    }

    public bool IsValid
    {
        get
        {
            if (!IsComplete) return false;
            //TODO - checksum (return true if checksum correct)
            return true;
        }
    }

    /// <summary>
    /// Adds bytes until the end of the array or a 0x55 code is read
    /// </summary>
    /// <param name="bytes"></param>
    /// <returns>The new offset to start the next segment at.</returns>
    public int AddBytes(int offset, byte[] bytes)
    {
        int bytePosition = offset;
        while (bytePosition < bytes.Length)
        {
            var currentByte = bytes[bytePosition];
            if (currentByte == StartCode)
            {
                IsComplete = true;
                break;
            }
            bytesInLine.Add(currentByte);
            bytePosition++;
        }
        return bytePosition;
    }
}

答案 1 :(得分:0)

将所有逻辑放在事件处理程序中可能不是最佳做法。我建议port_OnReceiveData只应关注提取72字节的消息。然后,您可能有另一个方法(或可能是另一个类)接受此消息并解析它并提取它包含的任何数据,然后可以将该数据传递给另一个方法或类,以便进一步处理或保存到数据库等。

我首先要开发port_OnReceiveData来可靠地捕获传入的消息。我认为你应该检查序列0x55 0x2f 0x48而不是0x55。据推测,0x55​​也可能出现在有效载荷的任何地方,而不仅仅是开始。您可以联系设备制造商(或查看他们的网站)了解消息协议的详细信息吗?

串口很难测试,因此想法是限制依赖于SerialPort逻辑的代码量,以便您可以单独测试您关心的实际代码(即消息的处理)。例如,一旦你捕获了一些72字节的消息,就可以保留它们并使用它们开发单元测试和逻辑,用于解析这些消息的代码,而无需PC或笔记本电脑必须物理连接到实验室设备

如果您真的想要测试逻辑,那么您可以做的另一件事是创建虚拟串行端口并传输已知的数据。此技术可用于确保您的逻辑正确捕获消息。例如,我曾经不得不从包括心率监测器在内的各种医疗设备中读取数据。然后我不得不在图表上绘制这些数据。这是正确的,这显然至关重要。我创建了一个测试应用程序,生成已知的波形图案:正弦波,锯齿波,扁平线等所有已知的振幅和频率。我创建了一个虚拟COM端口对,我的测试应用程序使用与真实设备相同的串行端口消息格式将此测试波形数据发送到com端口,我能够验证我在屏幕上捕获和输出的数据是否相同我已知的波形。

答案 2 :(得分:0)

我发现这个问题很有意思,所以在这种情况下,我做了一个简单的例子(有点天真,没有太多的计划)。 非常惯用的C#。 Something(MySerialMessageReader)负责读取数据,并使用解析良好的消息触发事件,供消费者做出反应。

希望有帮助吗?

public static class Program
{
    public static void Main(string[] args)
    {
        using (var reader = new MySerialMessageReader())
        {
            reader.MessageReaceived += (s, e) =>
            {
                // TODO: Process message / react to it. Probably put it in a queue to be processed
                Console.WriteLine("Received a new message: " + Encoding.UTF8.GetString(e.Message.Data));
            };

            Console.WriteLine("Waiting for data from serial port...");
            Console.ReadLine();
        }
    }
}

class MySerialMessageReader : IDisposable
{
    private byte[] mBuffer = new byte[72];
    private int mReceivedCount = 0;
    private bool mIsReadingMessage;

    SerialPort mPort = new SerialPort("COM12", 9600, Parity.None);

    public event EventHandler<MessageEventArgs> MessageReaceived;

    public MySerialMessageReader()
    {
    }

    public void Start()
    {
        mPort.DataReceived += Port_DataReceived;
        mPort.Open();
    }

    private void Port_DataReceived(object sender, SerialDataReceivedEventArgs e)
    {
        if (e.EventType == SerialData.Eof)
        {
            if (mIsReadingMessage && mReceivedCount < Message.MESSAGE_LENGTH)
            {
                Debug.Fail("Received EOF before an entire message was read.");
                return;
            }
        }

        // Put the read buffer into our cached buffer
        int n = 0;
        while (n < mPort.BytesToRead)
        {
            var readByte = (byte)mPort.ReadByte();

            if (!mIsReadingMessage && readByte == 0x55)
            {
                mIsReadingMessage = true; // New start of message
            }

            if (mIsReadingMessage)
            {
                // We're reading a message, put the message into a temporary buffer
                // whilst we read(/wait for) the rest of the message
                mBuffer[mReceivedCount++] = (byte)mPort.ReadByte();
            }

            if (mReceivedCount == Message.MESSAGE_LENGTH)
            {
                // We've got enough to construct a message
                Message m = null;
                if (Message.TryParse(mBuffer, ref m))
                {
                    if (this.MessageReaceived != null)
                        this.MessageReaceived(this, new MessageEventArgs(m));
                }
                else
                {
                    Console.WriteLine("Invalid message received. Checksum error?");
                }

                mReceivedCount = 0;
                Array.Clear(mBuffer, 0, 72);
            }
        }
    }

    public void Dispose()
    {
        if (mPort != null)
        {
            mPort.DataReceived -= Port_DataReceived;
            mPort.Dispose();
        }
    }
}

class Message
{
    private const int CHECKSUM_LENGTH = 3;
    private const int START_INDICATOR_LENGTH = 3;
    public const int MESSAGE_LENGTH = 72;

    public byte[] Data;
    public byte[] Checksum;

    private Message(byte[] data, byte[] checkSum)
    {
        this.Data = data;
        this.Checksum = checkSum;
    }

    public static bool TryParse(byte[] data, ref Message message)
    {
        if (data.Length != MESSAGE_LENGTH)
            return false; // Unexpected message length

        if (data[0] != 0x55 || data[1] != 0x2F || data[2] != 0x48)
            return false; // Invalid message start

        var withotStartIndicator = data.Skip(START_INDICATOR_LENGTH);

        var justData = withotStartIndicator.Take(MESSAGE_LENGTH - START_INDICATOR_LENGTH - CHECKSUM_LENGTH).ToArray();
        var justChecksum = withotStartIndicator.Skip(justData.Length).ToArray();

        // TODO: Insert checksum verification of justChecksum here
        // TODO: parse justData into whatever your message look like
        message = new Message(justData, justChecksum);

        return true;
    }
}

class MessageEventArgs : EventArgs
{
    public Message Message { get; set; }

    public MessageEventArgs(Message m)
    {
        this.Message = m;
    }
}

编辑:您可以使用async,队列以及其他任何内容,显然可以随意使用。但是如果你不是在阅读太多的数据,那么在事件处理程序中完成这一切并不是一件坏事(在我看来)。