Serial Port 系列(18) 基本篇 -- 發送回應(四)

這部份來處理接收的協定,接收的部份比起發送會更形複雜一些,先初步分解其工作為以下三大區塊。

       這部份來處理接收的協定,接收的部份比起發送會更形複雜一些,先初步分解其工作為以下三大區塊。

ProtocolRR04

 

       分析讀回的資料完整性

       在之前的  [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;
				}
			}
		}

 

       在這個方法中將會需要用到一個類別全域變數來記錄所取得的完整資料長度,所以要在類別中宣告一個變數 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 設計的部份,因為剩下的程式碼真的也不多了。