2011-09-08 44 views
3

我正在写一个需要与串口设备通信的.Net应用程序。该设备基本上是一些老式字母数字寻呼机的发射机。偶尔,我的应用程序需要打开一个串口并向发送器发送消息。如何组织与串口设备交谈的代码?

我知道谈话的设备的协议。这是一个来回“健谈”的协议。发送命令...等待一个特定的响应...发送另一个命令...等待另一个特定的响应...发送实际的消息...等待“接受”的响应。我可以通过一些非常黑客的代码来处理SerialPort对象上的一系列Write(...)方法调用,其间使用Thread.Sleep调用。

当然,我不想通过依靠Thread.Sleep来实际做到这一点,等待设备响应。看起来Reactive Extensions框架应该适合这种类型的事情,但是我很难解决这个问题。我开始与这一点,但很快就失去了和不知道下一步去哪里,或者如果这甚至是有道理的:

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

首先,我怎么踢这个东西了与第一serialPort.Write()呼叫?那么如何在发出下一个Write()调用之前检查预期的响应,将它们链接在一起?当然,如果我没有得到预期的回应,我会想要发生并抛出异常或其他东西。我甚至用Rx咆哮着正确的树,还是有另一种更适合这种模式的模式?谢谢!

回答

1

R x是抽象化过的“数据源推送数据”场景。在你的情况下,你已经将串口“read”方法建模为Rx observable,并且这需要与串口写方法结合使用。一种可能的解决方案如下所示,但可能需要根据应用程序的特定需求进行其他修改。

  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(); 
+0

我只是有点理解这一点,但我认为我会尝试实施它,看看它是否有效,如果是的话,将通过它来增加我的理解。但是,“receivedData.Subscribe(重放)”上存在编译错误;“线。编译器说:“不能从'System.Reactive.Subjects.ReplaySubject '转换为'System.IObserver >” –

+0

我在上一行有一个错误,我解决了这个问题,现在编写代码。它似乎工作,虽然它只能工作一次。不止一次运行应用程序总是会挂起,我从未收到过设备的好回复。不知道那里发生了什么 - 我正在关闭并在方法的末尾放置SerialPort对象。但这可能是一个单独的问题。这似乎是一个很好的方法。谢谢! –

+0

我有一个串行端口间谍应用程序运行,我认为这可能导致挂起。代码现在正在工作,尽管我在serialPort.Write(s)调用之后仍然使用Thread.Sleep()调用。似乎只需要一些时间来处理命令并发送回应,这是有道理的。如果我理解正确,DataReceived事件可能会在所有数据收到之前被多次触发,这可能是问题所在。 –

0

我没有发现RX非常适合这种串行通信。一般来说,RX似乎更多的是单向数据流,而不是来回协议。对于这样的串行通信,我围绕使用WaitHandles的串口编写了一个类,等待命令的响应。一般结构如下:

应用程序调用一个方法来启动异步操作以发送一系列命令。这将启动一个线程(我相信从线程池),轮流发送每个命令。一旦发送一个命令,操作就等待一个WaitHandle来获取响应(或者超时并重试或操作失败)。处理响应时,收到WaitHandle信号并发送下一个命令。

串行接收事件(在数据进入时在后台线程上运行)构建数据包。收到完整的数据包后,检查是否发送了命令。如果是这样,则发送新响应的线程并等待不同的WaitHandle来处理响应(这对于防止接收方甩掉响应数据很重要)。

编辑:增加了一个(稍大)示例显示两个核心发送和接收方法。

未显示Me.Receiver属性,它属于ISerialReceiver类型,负责构建数据包但不确定数据是否是正确的响应。同样未示出的是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 
+0

感谢您的回答。一般来说,我对C#非常熟练,但我真的没有太多的像这样的异步编程经验。这就是为什么我希望Rx可以稍微简化它。 :-)但我想我明白你的意思了:来回协议。 你有什么特定的代码示例可以分享如何实现这种模式?谢谢! –

+0

@Kevin Keubler:见编辑后。这只是发送/接收功能,所以你必须在'SendCommand'之上添加启动的异步操作。我使用'System.ComponentModel.AsyncOperationManager'在进度报告的后台线程上运行该方法。 –

+0

谢谢你的回答。 RX并不是来回的目标,而是尝试一下实验。让我们对流进行观察(好吧,如果TCP客户端有时可能需要重新连接,那么它们的序列就可以了,http://stackoverflow.com/questions/18978523/write-an-rx-retryafter-extension-method ),用EventLoopScheduler观察单独的线程,我们读取不恰当的字符串,我们使用Buffer()重新分组。总而言之,没有线程/'WaitHandle',我们可以用Observer方法处理协议的核心更高级别,只有更少的线程。有什么想法吗 ? –