這部份來處理接收的協定,接收的部份比起發送會更形複雜一些,先初步分解其工作為以下三大區塊。
這部份來處理接收的協定,接收的部份比起發送會更形複雜一些,先初步分解其工作為以下三大區塊。
分析讀回的資料完整性
在之前的 [Serial Port 系列(14) 基本篇 -- 完整接收資料(三)] 中我把資料完整性的程式碼寫在 Form 的相關程式碼中,但這一次要把它分出來放在這個處理接收資料的類別中,這有一個拗口的原因叫『降低藕合性』 (感謝小朱幫我正名),因為這樣做的話,當協定有變化的時候對於 Form 程式碼的影響程度會降低,省得改了東忘了西,然後花一堆時間找蟲。這部份的程式碼主要做兩件事:
(1) 從接收的部份找出長度碼,確認應該要讀回多少資料才取出。
(2) 將取得的資料轉成一個完整的陣列。
驗證資料的正確性
(1) 驗證開頭碼是否正確。
(2) 驗證整個陣列的長度是否正確。
(3) 驗證接收者是否正確。
(4) 驗證XOR碼是否正確。
(5) 不論驗證結果為正確或錯誤,觸發一個資料驗證的事件
解譯資料並回傳
(1) 分析回傳的命令碼,並做不同的處理
(1-1) 如果Device回傳一個DataError (99),觸發一個接收到錯誤資料的事件
(1-2) 如果是燈號控制,觸發一個命令執行成功或失敗的事件
(1-3) 如果是寫入資料,觸發一個命令執行成功或失敗的事件
(1-4) 如果是讀取資料,觸發一個回傳資料內容的事件
然後以下我們開始來循序地寫程式碼:
宣告類別並繼承
Public Class ReadProtocol
Inherits Protocol
class ReadProtocol : Protocol
分析讀回的資料完整性
Method GetReceiveDataFullLength:從傳入的 List<Byte> 中找出長度碼
Private Sub GetReceiveDataFullLength(ByVal tempList As List(Of Byte))
If tempList.Count >= 2 AndAlso innerReceiveDataFullLength = 0 Then
Dim startIndex As Int32 = tempList.IndexOf(Head)
If startIndex >= 0 AndAlso startIndex < (tempList.Count - 1) Then
innerReceiveDataFullLength = Convert.ToInt32(tempList(startIndex + 1)) + 2
End If
End If
End Sub
private void GetReceiveDataFullLength(List<Byte> tempList)
{
if (tempList.Count >= 2 && innerReceiveDataFullLength == 0)
{
Int32 startIndex = tempList.IndexOf(Head);
if (startIndex >= 0 && startIndex < tempList.Count - 1)
{
innerReceiveDataFullLength = Convert.ToInt32(tempList[startIndex + 1]) + 2;
}
}
}
在這個方法中將會需要用到一個類別裡的欄位 (field) 來記錄所取得的完整資料長度,所以要在類別中宣告一個變數 innerReceiveDataFullLength 。
Private innerReceiveDataFullLength As Int32
private Int32 innerReceiveDataFullLength;
Method GetFullReceiveData:從 tempList 中取出需要的部份轉換成 Byte 陣列,然後進行檢查與解譯
Private Function GetFullReceiveData(ByVal tempList As List(Of Byte)) As Boolean
If innerReceiveDataFullLength > 0 AndAlso tempList.Count >= tempList.IndexOf(Head) + innerReceiveDataFullLength Then
Dim temp(innerReceiveDataFullLength - 1) As Byte
Array.Copy(tempList.ToArray(), tempList.IndexOf(Head), temp, 0, temp.Length)
innerResponseFullBytes = temp
InterpretReceiveData()
innerReceiveDataFullLength = 0
Return True
Else
Return False
End If
End Function
private Boolean GetFullReceiveData(List<Byte> tempList)
{
if (innerReceiveDataFullLength > 0 && tempList.Count >= tempList.IndexOf(Head) + innerReceiveDataFullLength)
{
Byte[] temp = new Byte[innerReceiveDataFullLength];
Array.Copy(tempList.ToArray(), tempList.IndexOf(Head), temp, 0, temp.Length);
innerResponseFullBytes = temp;
InterpretReceiveData();
innerReceiveDataFullLength = 0;
return true;
}
else
{ return false; }
}
在這個方法中,我們使用一個 Boolean 值來表示是否有完成取出資料的行為,這邊也會用到一個類別欄位來記錄取出的陣列,所以要在類別中宣告一個變數 innerResponseFullBytes 。
Private innerResponseFullBytes As Byte()
private Byte[] innerResponseFullBytes;
各位有沒有注意到上面兩個方法的宣告都是 Private ?為了讓外部看起來簡單一些,所以把判斷是要取得長度還是要取資料的方法也寫在類別中,如此一來對於 Form 的程式碼而言會比較單純,於是來建立一個這類別中唯一的公開方法。
Method ArrangeReceiveData:
Public Function ArrangeReceiveData(ByVal tempList As List(Of Byte)) As Boolean
If innerReceiveDataFullLength = 0 Then
GetReceiveDataFullLength(tempList)
Return GetFullReceiveData(tempList)
Else
Return GetFullReceiveData(tempList)
End If
End Function
public Boolean ArrangeReceiveData(List<Byte> tempList)
{
if (innerReceiveDataFullLength == 0)
{
GetReceiveDataFullLength(tempList);
return GetFullReceiveData(tempList);
}
else
{
return GetFullReceiveData(tempList);
}
}
這個方法的邏輯在於如果現在 innerReceiveDataFullLength 的值為 0 ,那表示還未曾取得長度的資訊,於是就必須先做取得長度的程序,再來才是試著去取完整的資料。
驗證資料的正確性
Event DataChecked:建立一個事件來回應資料檢查的結果 (當然順便也建立一下其委派)
Public Delegate Sub DataCheckecdHandler(ByVal result As Boolean, ByVal checkedMessage As String)
Public Event DataChecked As DataCheckecdHandler
public delegate void DataCheckecdHandler(Boolean result, String checkedMessage);
public event DataCheckecdHandler DataChecked;
Property DataCheckedResult:建立一個資料檢查結果的屬性,它是public get;但卻是private set,因為不該讓外部更改這個值。
Public Property DataCheckedResult As Boolean
Get
Return innerDataCheckedResult
End Get
Private Set(value As Boolean)
innerDataCheckedResult = value
RaiseEvent DataChecked(innerDataCheckedResult, innerDataCheckedMessage)
End Set
End Property
public Boolean DataCheckedResult
{
get { return innerDataCheckedResult; }
private set
{
innerDataCheckedResult = value;
DataChecked(innerDataCheckedResult, innerDataCheckedMessage);
}
}
當這個值被設定時就會引發 DataChecked 事件回報檢查結果。
檢查各資料的方法
這個驗證的部份我用了一個很簡單的技巧去降低巢狀判斷式的複雜度 ,其實我本來考慮用 91 建議的方法 -- 抽一個介面來用,後來覺得怕把觀念一下展開的太多於是決定那個還是留到以後再講好了。言歸正傳,一般直覺可能都會寫出以下這種可怕 (或許『可怕』不是個好的形容詞) 的巢狀判斷式 :
Private Function CheckData() As Boolean
If Not innerResponseFullBytes Is Nothing Then
If CheckHead() = True Then
If CheckLength() = True Then
If CheckReceiver() = True Then
If CheckXOR() = True Then
innerDataCheckedMessage = "正確回傳"
DataCheckedResult = True
Else
DataCheckedResult = False
End If
Else
DataCheckedResult = False
End If
Else
DataCheckedResult = False
End If
Else
DataCheckedResult = False
End If
Else
innerDataCheckedMessage = "無回傳資料"
DataCheckedResult = False
End If
Return DataCheckedResult
End Function
Private Function CheckHead() As Boolean
If innerResponseFullBytes(0) = Head Then
Return True
Else
innerDataCheckedMessage = "開頭碼錯誤"
Return False
End If
End Function
Private Function CheckLength() As Boolean
If innerResponseFullBytes.Length - 2 = innerResponseFullBytes(1) Then
innerResponseContentLength = innerResponseFullBytes.Length - 5 '減去開頭, 長度, 接收者, 命令, XOR
Return True
Else
innerDataCheckedMessage = "長度錯誤"
Return False
End If
End Function
Private Function CheckReceiver() As Boolean
If innerResponseFullBytes(2) = Direction.DeviceToPC Then
Return True
Else
innerDataCheckedMessage = "接收者錯誤"
Return False
End If
End Function
Private Function CheckXOR() As Boolean
If innerResponseFullBytes(innerResponseFullBytes.Length - 1) = GetXorValue(innerResponseFullBytes) Then
Return True
Else
innerDataCheckedMessage = "XOR檢查碼錯誤"
Return False
End If
End Function
那該怎麼簡化,最簡單的思考方式就是當第一關過了就回傳第二關的結果、當第二關過了就回傳第三關的結果……,於是程式碼就簡化了。
Private Function CheckData() As Boolean
If Not innerResponseFullBytes Is Nothing Then
DataCheckedResult = CheckHead()
Else
innerDataCheckedMessage = "無回傳資料"
DataCheckedResult = False
End If
Return DataCheckedResult
End Function
Private Function CheckHead() As Boolean
If innerResponseFullBytes(0) = Head Then
Return CheckLength()
Else
innerDataCheckedMessage = "開頭碼錯誤"
Return False
End If
End Function
Private Function CheckLength() As Boolean
If innerResponseFullBytes.Length - 2 = innerResponseFullBytes(1) Then
innerResponseContentLength = innerResponseFullBytes.Length - 5 '減去開頭, 長度, 接收者, 命令, XOR
Return CheckReceiver()
Else
innerDataCheckedMessage = "長度錯誤"
Return False
End If
End Function
Private Function CheckReceiver() As Boolean
If innerResponseFullBytes(2) = Direction.DeviceToPC Then
Return CheckXOR()
Else
innerDataCheckedMessage = "接收者錯誤"
Return False
End If
End Function
Private Function CheckXOR() As Boolean
If innerResponseFullBytes(innerResponseFullBytes.Length - 1) = GetXorValue(innerResponseFullBytes) Then
innerDataCheckedMessage = "正確回傳"
Return True
Else
innerDataCheckedMessage = "XOR檢查碼錯誤"
Return False
End If
End Function
private Boolean CheckData()
{
if (innerResponseFullBytes != null)
{
DataCheckedResult = CheckHead();
}
else
{
innerDataCheckedMessage = "無回傳資料";
DataCheckedResult = false;
}
return DataCheckedResult;
}
private Boolean CheckHead()
{
if (innerResponseFullBytes[0] == Head)
{
return CheckLength();
}
else
{
innerDataCheckedMessage = "開頭碼錯誤";
return false;
}
}
private Boolean CheckLength()
{
if (innerResponseFullBytes.Length - 2 == innerResponseFullBytes[1])
{
innerResponseContentLength = innerResponseFullBytes.Length - 5; //減去開頭, 長度, 接收者, 命令, XOR
return CheckReceiver();
}
else
{
innerDataCheckedMessage = "長度錯誤";
return false;
}
}
private Boolean CheckReceiver()
{
if (innerResponseFullBytes[2] == (Byte)Direction.DeviceToPC)
{
return CheckXOR();
}
else
{
innerDataCheckedMessage = "接收者錯誤";
return false;
}
}
private Boolean CheckXOR()
{
if (innerResponseFullBytes[innerResponseFullBytes.Length - 1] == GetXorValue(innerResponseFullBytes))
{
innerDataCheckedMessage = "正確回傳";
return true;
}
else
{
innerDataCheckedMessage = "XOR檢查碼錯誤";
return false;
}
}
有沒有覺得看起來清爽很多,當然還有更清爽的,如果你懂得如何抽象化『檢查』這檔事的話。
這邊又需要再建立兩個類別全域變數,一個是做為 DataCheckedResult 使用的變數;另一個則是儲存檢查結果的訊息字串。
Private innerDataCheckedResult As Boolean
Private innerDataCheckedMessage As String
private Boolean innerDataCheckedResult;
private String innerDataCheckedMessage;
解譯資料並回傳
我們先來建立前面所分析的幾個所需的事件和相關委派,依序為資料錯誤、成功或失敗、資料回應。
Public Delegate Sub ReceiveErrorDataHandler()
Public Event ReceiveErrorData As ReceiveErrorDataHandler
Public Delegate Sub ReceiveActionResultHandler(ByVal result As Boolean, ByVal command As CommandCode)
Public Event ReceiveActionResult As ReceiveActionResultHandler
Public Delegate Sub ReceiveReturnDataHandler(ByVal content As Byte(), ByVal command As CommandCode)
Public Event ReceiveReturnData As ReceiveReturnDataHandler
public delegate void ReceiveErrorDataHandler();
public event ReceiveErrorDataHandler ReceiveErrorData;
public delegate void ReceiveActionResultHandler(Boolean result, CommandCode command);
public event ReceiveActionResultHandler ReceiveActionResult;
public delegate void ReceiveReturnDataHandler(Byte[] content, CommandCode command);
public event ReceiveReturnDataHandler ReceiveReturnData;
在前面 GetFullReceiveData 方法中有一個沒有講到的東西就是呼叫 InterpretReceiveData(),這個就是進入解譯資料的第一個 Method
Method InterpretReceiveData:先做資料正確性檢查,若沒有問題則分析其命令碼引發不同事件
Private Sub InterpretReceiveData()
If CheckData() = True Then
innerResponseCommand = DirectCast(innerResponseFullBytes(3), CommandCode)
Select Case innerResponseCommand
Case CommandCode.ReadBlock
InterpretReadBlock()
Case CommandCode.WriteBlock
RaiseEvent ReceiveActionResult(Convert.ToBoolean(innerResponseFullBytes(4)), innerResponseCommand)
Case CommandCode.LightControl
RaiseEvent ReceiveActionResult(Convert.ToBoolean(innerResponseFullBytes(4)), innerResponseCommand)
Case CommandCode.DataError
RaiseEvent ReceiveErrorData()
Case Else
Throw New ArgumentOutOfRangeException()
End Select
End If
End Sub
private void InterpretReceiveData()
{
if (CheckData())
{
innerResponseCommand = (CommandCode)innerResponseFullBytes[3];
switch (innerResponseCommand)
{
case CommandCode.ReadBlock:
InterpretReadBlock();
break;
case CommandCode.WriteBlock:
ReceiveActionResult(Convert.ToBoolean(innerResponseFullBytes[4]), innerResponseCommand);
break;
case CommandCode.LightControl:
ReceiveActionResult(Convert.ToBoolean(innerResponseFullBytes[4]), innerResponseCommand);
break;
case CommandCode.DataError:
ReceiveErrorData();
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
這個方法只有一個要附帶提的是當收到的結果包含讀回的 Block 資料時,會呼叫另一個方法 InterpretReadBlock() 來取出單純只有 Block 資料的部份再引發事件。
Method InterpretReadBlock:從回傳的資料中取出 Block Data再引發事件
Private Sub InterpretReadBlock()
Dim content(innerResponseContentLength - 1) As Byte
Array.Copy(innerResponseFullBytes, 4, content, 0, content.Length)
RaiseEvent ReceiveReturnData(content, innerResponseCommand)
End Sub
private void InterpretReadBlock()
{
Byte[] content = new Byte[innerResponseContentLength];
Array.Copy(innerResponseFullBytes, 4, content, 0, content.Length);
ReceiveReturnData(content, innerResponseCommand);
}
類別全域變數:
Private innerResponseCommand As CommandCode
private CommandCode innerResponseCommand;
這一篇講完了接收處理的部份,預計下一篇應該就可以講完 Form 設計的部份,因為剩下的程式碼真的也不多了。