zoukankan      html  css  js  c++  java
  • 设计一个串口装饰类(1)

          团队正在开发一个仪器控制软件的框架,希望该框架能兼容/容忍一些硬件的变换,以及灵活定制建立在该硬件平台之上的工作流。目标仪器使用了很多的串口通信(Serial Port),所以大家觉得应该设计/封装一个统一的串口类来管理串口通信的一致性。就我个人的意见来说,我不是建议在System.IO.Port.SerialPort上再做封装的。串口通信逻辑很简单,基本就是I/O。该类已经提供了同步阻塞模型、基于事件的异步模型,各种I/O快捷方法,所以不认为封装该类可以获得什么更多好处。但是面对框架的 一些其他需求,比如希望记录打开的串口的设备信息,或者在串口IO时,在一个点记录串口的工作状态也是一个不错的选择。记录打开的设备信息可以借由工厂模式打开串口,而IO的工作状态记录并不轻松——Serial POrt的IO已经很丰富,要完全包裹并不是一个好的注意。

    我反对建立一个串口装饰类的原因:

    1. SerialPort工作模式为全双工,基于事件的工作模型。如果装饰类将这些封装,要么间接将工作模型全部暴露出来,要么将阉割工作模式,比如变成全阻塞模式,单工模式。
    2. 如果在装饰类中间接兼容SerialPort的工作模型,这等于重新发明轮子。
    3. 如果阉割工作模型,变为单工、应答模式,当设备存在主动上报状态(主动Send)时,主动上报信息将会丢失。

    虽然如此,但团队似乎有意将串口工作在主从、应答式单工模式之下。其一是因为该工作方式简单,其二是因为仪器App需要与电子组自行设计的IO板进行串口通信,它们只支持主从-应答模式。在确定工作模型之后,还提出希望提供快捷I/O方法,方便设备类与设备进行通信。它们的期望是:

    1. 提供收发一体的阻塞函数。设备类只提供指令,通关快捷方法进行发送,并返回响应结果。
    2. 在收发一体的阻塞函数里实现响应结果的帧检测,如果遇到没有Read完全的帧进行继续读取,直到读取到完整的帧为止。
    3. 在收发一体的函数里实现命令的超时检测功能。当某一命令接收太慢时,抛出TimeoutException。
    4. 在收发一体的函数里实现失败重发的功能。

    这些要求很适合通过串口的装饰类来实现,工作在主从-应答的单工模式之下实现起来需要考虑的较少,也比较简单。经过讨论之后,我觉得始终有 一个疑问,那就是之前老版本实现的“完整帧读取”的逻辑(关于2)。

    为了保证失败重发,在于与IO板的I/O内容自定义了协议。但协议没有定义结束符号,只定义了开始符、头部、长度等。因为没有结束符,所以在收发 一体的快捷函数里,判断读取到的字节数组是否是一个完整的帧,就变成了一个问题。因为收发一体的函数逻辑是这样的:(并不是按照协议逐字段读取的,而是批量读取,虽然我觉得他们也够懒得,但确实这么做了...)

    //伪代码 1
    public static byte[] Talk(....)
    {
        write();
        return read();
    }

    当没有结束符的时候,就没有办法在read的byte[]中判断一个命令的响应帧是否结束了。为了保证是一个完成的帧,又采用了完整帧检测的方法:

    //伪代码 2
    public static byte[] Talk(....)
    {
        write();
        var str = new StringBuilder();
        do
        {
            str.append(read());
        }while(!Check(str)) //or timeout..      
    }

    伪代码的逻辑是,读取存在字节,然后检测是否是一个完整的可解析的协议帧,如果是,则返回;如果不是,则增量读取,直至超时。超时之后,读到的数据会被丢弃。(是吧,很诡异吧!)

    面对这种形式,我个人始终有两个困惑:1,为了判断帧边界弄得如此复杂;2,当增量读取遇到两次连续实时响应时,将出错。先说问题2,这达成了一致,确实存在这种情况。但既然工作模型定义在主从-应答模式,就不存在连续两次的响应了。

    对于问题1,我个人这始终有很多的疑问,但限于硬件开发的经验的单薄,只能向他人请教。首先初略看了一下SerialPort的函数,读取大概有这么几个:

    public Read(Byte[], Int32, Int32)
    public string ReadLine()
    public string ReadExisting()

    ReadLine()这个函数似乎很好理解,读取一行,即一直读直到遇到 (or )。在使用串口设备调试App时,一般可以选择结束符,应该就是这个意思。对于厂家的设备,如果采用DT模式,该函数毫无疑问很适合。但面对自定义的协议,不一定可靠,即自定义协议不一定发送 作为结束符,此函数不合适。

    public Read(Byte[], Int32, Int32)我觉得也可以保证读取到完整的帧。因为默认情况下,接收缓冲区只有4096个字节。但是同事反驳的理由是读取缓冲区的内容不一定是全部,可能还有一部分在路上。我个人不太赞同这个观点,只是我觉得每次读取最大缓冲数量的字节都需要超时返回,通信时间太长,并不适合。

    public string ReadExisting() 从名字来看,读取了内部缓冲区的所有数据,应该是完整的帧,但他仍然觉得还有部分数据仍然在路上。

    最开始我想从一般的硬件通信流程来思考这个过程,比如当发送方发送了数据,这些数据是全部到了接收方的内部缓冲才通知Read开始工作呢,还是一有数据就开始工作。如果是前者,ReadExisting应该没有问题,如果是后者,好像也不行。当然,同事和我无法确定这个过程的逻辑。

    我这个人真是倔强,始终认为伪代码2是一个不好的实现方法。只能再找方法,寻找确认一个帧已经读取完毕的方法。很不幸,ReadExisting这个方法的名字真是取得太烂了。关于ReadExisting方法, MSDN是这么描述的:

    在编码的基础上,读取 SerialPort 对象的流和输入缓冲区中所有立即可用的字节。
    Reads all immediately available bytes, based on the encoding, in both the stream and the input buffer of the SerialPort object.

    中文、英文都贴上,意思是说该函数不光读取当前缓冲区上的所有字节,还将读完流(stream)上的所有字节。那么读完流上的字节是怎么处理呢?查看其源代码:

    public string ReadExisting()
            {
                if (!IsOpen)
                    throw new InvalidOperationException(SR.GetString(SR.Port_not_open));
     
                byte [] bytesReceived = new byte[BytesToRead];
     
                if (readPos < readLen)
                {           // stuff in internal buffer
                    Buffer.BlockCopy(inBuffer, readPos, bytesReceived, 0, CachedBytesToRead);
                }
                internalSerialStream.Read(bytesReceived, CachedBytesToRead, bytesReceived.Length - (CachedBytesToRead));    // get everything
                // Read full characters and leave partial input in the buffer. Encoding.GetCharCount doesn't work because
                // it returns fallback characters on partial input, meaning that it overcounts. Instead, we use 
                // GetCharCount from the decoder and tell it to preserve state, so that it returns the count of full 
                // characters. Note that we don't actually want it to preserve state, so we call the decoder as if it's 
                // preserving state and then call Reset in between calls. This uses a local decoder instead of the class 
                // member decoder because that one may preserve state across SerialPort method calls.

    该方法首先拷贝了当前缓冲区的所有内容,接着开始读取流上的内容。一共读取的字节数量为BytesToRead。BytesToRead属性的定义为:

         public int BytesToRead
            {
                get
                {
                    if (!IsOpen)
                        throw new InvalidOperationException(SR.GetString(SR.Port_not_open));
                    return internalSerialStream.BytesToRead + CachedBytesToRead; // count the number of bytes we have in the internal buffer too.
                }
            } 
    // internalSerialStream.BytesToRead的定义:
    // Fills comStat structure from an unmanaged function
            // to determine the number of bytes waiting in the serial driver's internal receive buffer.
            internal int BytesToRead {
                get
                {
                    int errorCode = 0; // "ref" arguments need to have values, as opposed to "out" arguments
                    if (UnsafeNativeMethods.ClearCommError(_handle, ref errorCode, ref comStat)  == false)
                    {
                        InternalResources.WinIOError();
                    }
                    return (int) comStat.cbInQue;
                }
            }

    看到此,可以确定ReadExisting一定能读取到至少一个发送方发送的一个完整的数组(Write(Byte[], Int32, Int32) OR Write(string)),而这个数组在绝大大多数情况下就是一个完整的帧。ReadExisting应该被认为是至少一次完整的IO过程,即使其最终读取到的数据可能是两个响应命令,因为它还读取了当前已缓冲的数据。所以在伪代码2中,Check()不应该用来做一个帧边界检查,而是应该对两个帧作边界检查。如果串口工作在主动-应答的模式下,ReadExisting读取到的一定是一个完整的响应帧。在双工模式下,它可能读到的是多个响应;在大数据分次传输时,它相对于大文件可能是不完整的。

    所以,不要担心一个帧有没有收完,还是担心多个帧如何分离吧。:)

    在解决了以上的疑问之后,可以开始实现串口的装饰类了。(待续)

    ***这不是一个好的主意,很失败。本来是私用的偷懒的写法,却引起许多新的问题或者争论。总的来说,这不够专业。***

  • 相关阅读:
    leetcode 48. Rotate Image
    leetcode 203. Remove Linked List Elements 、83. Remove Duplicates from Sorted List 、82. Remove Duplicates from Sorted List II(剑指offer57 删除链表中重复的结点) 、26/80. Remove Duplicates from Sorted ArrayI、II
    leetcode 263. Ugly Number 、264. Ugly Number II 、313. Super Ugly Number 、204. Count Primes
    leetcode 58. Length of Last Word
    安卓操作的一些问题解决
    leetcode 378. Kth Smallest Element in a Sorted Matrix
    android studio Gradle Build速度加快方法
    禁用gridview,listview回弹或下拉悬停
    Android Studio找不到FragmentActivity类
    安卓获取ListView、GridView等滚动的距离(高度)
  • 原文地址:https://www.cnblogs.com/jjseen/p/5544330.html
Copyright © 2011-2022 走看看