2011-09-08 43 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方法處理協議的核心更高級別,只有更少的線程。有什麼想法嗎 ? –