简体   繁体   English

如何组织代码与串口设备通信?

[英]How can I organize code for talking to a serial port device?

I'm writing a .Net app that will need to talk to a serial port device. 我正在编写一个需要与串口设备通信的.Net应用程序。 The device is basically a transmitter for some old school alphanumeric pagers. 该设备基本上是一些旧式字母数字寻呼机的发射器。 Occasionally, my app will need to open up a serial port and send a message to the transmitter. 有时,我的应用程序需要打开一个串口并向发送器发送消息。

I know the protocol for talking to the device. 我知道与设备通话的协议。 It's a bit of a back and forth "chatty" protocol. 这是一个来回的“聊天”协议。 Send a command ... wait for a particular response ... send another command ... wait for another particular response ... send the actual message ... wait for an "accepted" response. 发送命令...等待特定响应...发送另一个命令...等待另一个特定响应...发送实际消息...等待“接受”响应。 I can get this to work with some really hacky code involving a series of Write(...) method calls on a SerialPort object, with Thread.Sleep calls in between. 我可以使用一些非常hacky的代码来处理SerialPort对象上的一系列Write(...)方法调用,其间有Thread.Sleep调用。

Of course, I don't want to actually do this by relying on Thread.Sleep to wait for the device to respond. 当然,我不想通过依赖Thread.Sleep等待设备响应来实际执行此操作。 It seems like the Reactive Extensions framework should be suited for this type of thing, but I'm having trouble getting my head around it. 似乎Reactive Extensions框架应该适合这种类型的东西,但是我无法理解它。 I started with this, but quickly got lost and wasn't sure where to go next, or if this even makes sense: 我从这开始,但很快迷路了,不知道下一步该去哪里,或者这是否有意义:

var receivedData = Observable.FromEventPattern<SerialDataReceivedEventArgs>(serialPort, "DataReceived");
receivedData
    .Where(d => d.EventArgs.EventType == SerialData.Chars)
    .Subscribe(args =>
            {
                var response = serialPort.ReadExisting();
                // Now what?
            });

First, how do I kick this thing off with the first serialPort.Write() call? 首先,如何通过第一次serialPort.Write()调用来解决这个问题? Then how do I chain them together by checking for the expected response before issuing the next Write() call? 那么如何通过在发出下一个Write()调用之前检查预期的响应来将它们链接在一起? And of course if I don't get the expected response I'd want to break out and throw an exception or something. 当然,如果我没有得到预期的响应,我想要突破并抛出异常或其他东西。 Am I even barking up the right tree with Rx, or is there another pattern that would be better suited for this? 我甚至用Rx咆哮着正确的树,还是有另一种更适合这种情况的模式? Thanks! 谢谢!

Rx is abstraction over "data source pushing data" scenarios. Rx是“数据源推送数据”场景的抽象。 In your case you have modeled the serial port "read" method as Rx observable and this need to be combined with serial port write method. 在您的情况下,您已将串行端口“读取”方法建模为Rx observable,这需要与串行端口写入方法结合使用。 One possible solution would be something like below, although it may require some other modifications based on the specific needs of your application. 一种可能的解决方案如下所示,尽管可能需要根据您的应用程序的特定需求进行一些其他修改。

            var serialPort = new System.IO.Ports.SerialPort("COM1");
            serialPort.Open();
            var receivedData = Observable.FromEvent<SerialDataReceivedEventArgs>(serialPort, "DataReceived")
                               .Where(d => d.EventArgs.EventType == SerialData.Chars)
                               .Select(_ => serialPort.ReadExisting());
            var replay = new ReplaySubject<string>();
            receivedData.Subscribe(replay);
            var commands = (new List<string>() { "Init", "Hello", "Done" });
            commands.Select((s, i) =>
            {
                serialPort.Write(s);
                var read = replay.Skip(i).First();
                //Validate s command against read response
                return true;//To some value to indicate success or failure
            }).ToList();

The workflow of sending and receiving can be nicely handled by combining Rx and async/await. 通过组合Rx和async / await可以很好地处理发送和接收的工作流程。 I blogged about this at 我在博客上写了这篇文章 http://kerry.lothrop.de/serial-rx/ . http://kerry.lothrop.de/serial-rx/

I don't find RX to be a great fit for this kind of serial communication. 我发现RX不适合这种串行通信。 In general RX seems to be more about one way streams of data, rather than back and forth protocols. 通常,RX似乎更多地是关于单向数据流,而不是来回协议。 For serial communications like this, I wrote a class around the serial port that uses WaitHandles to wait for a response to commands. 对于像这样的串行通信,我在串口上写了一个类,它使用WaitHandles来等待对命令的响应。 The general structure is: 一般结构是:

The application calls a method to launch an async operation to send a sequence of commands. 应用程序调用一个方法来启动异步操作以发送一系列命令。 This will launch a thread (from the thread pool, I believe) that sends each command in turn. 这将启动一个线程(我相信来自线程池),它依次发送每个命令。 Once a command is sent, the operation waits on a WaitHandle to get the response (or timeout and do retries or fail the operation). 发送命令后,操作将等待WaitHandle以获取响应(或超时并执行重试或操作失败)。 When the response is processed, the receive WaitHandle is signaled and the next command is sent. 处理响应时,将发信号通知接收WaitHandle并发送下一个命令。

The serial receive event (which runs on background threads whenever data comes in) builds up packets of data. 串行接收事件(在数据进入时在后台线程上运行)构建数据包。 When a complete packet is received, check if a command was sent. 收到完整数据包后,检查是否发送了命令。 If so, signal sending thread of the new response and wait on a different WaitHandle to let the response be processed (which can be important to prevent the receiver from trashing the response data). 如果是,则发信号通知新响应的发送线程并等待不同的WaitHandle以处理响应(这对于防止接收器丢弃响应数据很重要)。

EDIT: Added a (somewhat large) sample showing the two core send and receive methods. 编辑:添加了一个(有点大)样本,显示了两个核心发送和接收方法。

Not shown are the Me.Receiver property, which is of type ISerialReceiver and is responsible for building packets of data, but not determining if the data is the correct response. 未显示的是Me.Receiver属性,它是ISerialReceiver类型,负责构建数据包,但不确定数据是否是正确的响应。 Also not shown are CheckResponse and ProcessIncoming which are two abstract methods overriden by derived classes to determine if the response is to the command just sent and handle "unsolicited" incoming packets, respectively. 同样未示出的是CheckResponse和ProcessIncoming,它们是两个抽象方法,它们被派生类覆盖,以确定响应是否是刚刚发送的命令,并分别处理“未经请求的”传入数据包。

''' <summary>This field is used by <see cref="SendCommand" /> to wait for a
''' response after sending data.  It is set by <see cref="ReceiveData" />
''' when <see cref="ISerialReceiver.ProcessResponseByte">ProcessResponseByte</see>
''' on the <see cref="Receiver" /> returns true.</summary>
''' <remarks></remarks>
Private ReceiveResponse As New System.Threading.AutoResetEvent(False)
''' <summary>This field is used by <see cref="ReceiveData" /> to wait for
''' the response to be processed after setting <see cref="ReceiveResponse" />.
''' It is set by <see cref="SendCommand" /> when <see cref="CheckResponse" />
''' returns, regardless of the return value.</summary>
''' <remarks></remarks>
Private ProcessResponse As New System.Threading.ManualResetEvent(True)
''' <summary>
''' This field is used by <see cref="SendCommand" /> and <see cref="ReceiveData" />
''' to determine when an incoming packet is a response packet or if it is
''' one of a continuous stream of incoming packets.
''' </summary>
''' <remarks></remarks>
Private responseSolicited As Boolean

''' <summary>
''' Handles the DataReceived event of the wrapped SerialPort.
''' </summary>
''' <param name="sender">The wrapped SerialPort that raised the event.
''' This parameter is ignored.</param>
''' <param name="e">The event args containing data for the event</param>
''' <remarks>This function will process all bytes according to the
''' <see cref="Receiver" /> and allow <see cref="SendCommand" /> to
''' continue or will call <see cref="ProcessIncoming" /> when a complete
''' packet is received.</remarks>
Private Sub ReceiveData(ByVal sender As Object, ByVal e As SerialDataReceivedEventArgs)
    If e.EventType <> SerialData.Chars Then Exit Sub
    Dim input() As Byte

    SyncLock _portLock
        If Not _port.IsOpen OrElse _port.BytesToRead = 0 Then Exit Sub
        input = New Byte(_port.BytesToRead - 1) {}
        _port.Read(input, 0, input.Length)
    End SyncLock

    'process the received data
    If input Is Nothing OrElse input.Length = 0 OrElse Me.Receiver Is Nothing Then Exit Sub

    Dim responseCompleted As Boolean

    For i As Integer = 0 To input.Length - 1
        responseCompleted = Me.Receiver.ProcessResponseByte(input(i))

        'process completed response
        If responseCompleted Then
            responseSolicited = False
            System.Threading.WaitHandle.SignalAndWait(ReceiveResponse, ProcessResponse)

            'the data is not a response to a command sent by the decoder
            If Not responseSolicited Then
                ProcessIncoming(Me.Receiver.GetResponseData())
            End If
        End If
    Next
End Sub

''' <summary>
''' Sends a data command through the serial port.
''' </summary>
''' <param name="data">The data to be sent out the port</param>
''' <returns>The data received from the port or null if the operation
''' was cancelled.</returns>
''' <remarks>This function relies on the Receiver 
''' <see cref="ISerialReceiver.GetResponseData">GetResponseData</see> and 
''' the overriden <see cref="CheckResponse" /> to determine what response 
''' was received and if it was the correct response for the command.
''' <seealso cref="CheckResponse" /></remarks>
''' <exception cref="TimeoutException">The operation timed out.  The packet
''' was sent <see cref="MaxTries" /> times and no correct response was received.</exception>
''' <exception cref="ObjectDisposedException">The SerialTransceiver was disposed before
''' calling this method.</exception>
Private Function SendCommand(ByVal data() As Byte, ByVal ignoreCancelled As Boolean) As Byte()
    CheckDisposed()
    If data Is Nothing Then Return Nothing

    'make a copy of the data to ensure that it does not change during sending
    Dim sendData(data.Length - 1) As Byte
    Array.Copy(data, sendData, data.Length)

    Dim sendTries As Integer = 0
    Dim responseReceived As Boolean
    Dim responseData() As Byte = Nothing
    ReceiveResponse.Reset()
    ProcessResponse.Reset()
    While sendTries < MaxTries AndAlso Not responseReceived AndAlso _
          (ignoreCancelled OrElse Not Me.IsCancelled)
        'send the command data
        sendTries += 1
        If Not Me.WriteData(sendData) Then Return Nothing

        If Me.Receiver IsNot Nothing Then
            'wait for Timeout milliseconds for a response.  If no response is received
            'then waitone will return false.  If a response is received, the AutoResetEvent
            'will be triggered by the SerialDataReceived function to return true.
            If ReceiveResponse.WaitOne(Timeout, False) Then
                Try
                    'get the data that was just received
                    responseData = Me.Receiver.GetResponseData()
                    'check to see if it is the correct response
                    responseReceived = CheckResponse(sendData, responseData)
                    If responseReceived Then responseSolicited = True
                Finally
                    'allow the serial receive function to continue checking bytes
                    'regardless of if this function throws an error
                    ProcessResponse.Set()
                End Try
            End If
        Else
            'when there is no Receiver, assume that there is no response to
            'data sent from the transceiver through this method.
            responseReceived = True
        End If
    End While

    If Not ignoreCancelled AndAlso Me.IsCancelled Then
        'operation was cancelled, return nothing
        Return Nothing
    ElseIf Not responseReceived AndAlso sendTries >= MaxTries Then
        'operation timed out, throw an exception
        Throw New TimeoutException(My.Resources.SerialMaxTriesReached)
    Else
        'operation completed successfully, return the data
        Return responseData
    End If
End Function

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM