Serial Port 系列(14) 基本篇 -- 完整接收資料(三)

情境三:具備長度資料
有些協定是會帶長度資料的,以下的例子在第一個Byte 所帶的開頭碼為十進位值 249 ,第二個Byte 所代表的是後面資料內容的長度。

       情境三:具備長度資料

       有些協定是會帶長度資料的,以下的例子在第一個Byte 所帶的開頭碼為十進位值 249 ,第二個Byte 所代表的是後面資料內容的長度。

Protocol_Length

 

       既然資料中帶了長度碼,我們就可以根據這個值來決定如何解析資料。跟之前一樣我們先來改裝一下發送的部份:

 

       設定一個開頭碼常數

       因為懶惰與怕打錯字的因素,所以先為開頭碼設定一個常值:


Private Const head As Byte = 249

 


private const Byte head = 249;

 

       Method GetSendBuffer

       這次來傳送的是一般字串,以 UTF8 形式編碼成 Byte 陣列後傳出去,所以建立這個方法來做轉換


	Private Function GetSendBuffer(ByVal content As String) As Byte()
		Dim dataBytes() As Byte = Encoding.UTF8.GetBytes(content)
		If dataBytes.Length < 256 Then
			Dim result(dataBytes.Length + 1) As Byte
			result(0) = head
			result(1) = Convert.ToByte(dataBytes.Length)
			Array.Copy(dataBytes, 0, result, 2, dataBytes.Length)
			Return result
		Else
			Throw New OverflowException
		End If
	End Function

 


		private Byte[] GetSendBuffer(String content)
		{
			Byte[] dataBytes = Encoding.UTF8.GetBytes(content);
			if (dataBytes.Length < 256)
			{
				Byte[] result = new Byte[dataBytes.Length + 2];
				result[0] = head;
				result[1] = Convert.ToByte(dataBytes.Length);
				Array.Copy(dataBytes, 0, result, 2, dataBytes.Length);
				return result;
			}
			else
			{
				throw new OverflowException();
			}
		}

 

       上面的程式碼中首先要看到的是使用了  [Encoding 類別] 並且採 UTF8 編碼來做轉換;另外一個重點是不能讓轉換後的 Byte 陣列長度超過 255,因為我們只用一個 Byte 來存長度。再來呢我們設一個新的 Byte 陣列 result 其總長度為資料長度加 2 ,然後設定開頭碼、長度碼並將資料複製進去做為傳送的整個內容。

 

       Method SendData

       先建立一個具有五個元素的字串陣列,這些字串會經由 GetSendBuffer 方法轉換為協定規範的 Byte 陣列後傳送出去。

 

 


	Private Sub SendData(ByVal port As Object)
		Dim buffer() As Byte
		Dim content() As String = New String() {"範例:", "Serial Port 測試", "協定帶長度字元", "By Visual Basic", "結束"}
		For i As Int32 = 0 To content.Length - 1
			buffer = GetSendBuffer(content(i))
			Try
				DirectCast(port, SerialPort).Write(buffer, 0, buffer.Length)
				'	Thread.Sleep(500)
			Catch ex As Exception
				'這邊你可以自訂發生例外的處理程序
				CloseComport(DirectCast(port, SerialPort))
				MessageBox.Show(String.Format("出問題啦:{0}", ex.ToString()))
			Finally
				sending = False
			End Try
		Next
	End Sub

 


		private void SendData(Object port)
		{
			Byte[] buffer;
			String[] content = new String[] { "範例:", "Serial Port 測試", "協定帶長度字元", "By Visual C#", "結束" };
			for (Int32 i = 0; i <= content.Length - 1; i++)
			{
				buffer = GetSendBuffer(content[i]);
				try
				{
					(port as SerialPort).Write(buffer, 0, buffer.Length);
				}
				catch (Exception ex)
				{
					//這邊你可以自訂發生例外的處理程序
					CloseComport((port as SerialPort));
					MessageBox.Show(String.Format("出問題啦:{0}", ex.ToString()));
				}
				finally
				{
					sending = false;
				}
			}
		}

 

       緊接著來修改讀取端的程式,一樣以[Serial Port 系列(11) 基本篇 -- 利用執行緒讀取資料] 為範本。

 

       設定一個開頭碼常數

       如發送端一樣,請先加上 head 常數的宣告。

 

       Method DisplayText

       因為這次的範例發送端改採 UTF8 編碼,所以接收端也得改成一樣的編碼形式才行。

 


	Private Sub DisplayText(ByVal buffer As Byte())
		TextBox1.Text &= String.Format("{0}{1}", Encoding.UTF8.GetString(buffer), Environment.NewLine)
		totalLength = totalLength + buffer.Length
		Label2.Text = totalLength.ToString()
	End Sub

 

 


		private void DisplayText(Byte[] buffer)
		{
			TextBox1.Text += String.Format("{0}{1}", Encoding.UTF8.GetString(buffer), Environment.NewLine);
			totalLength = totalLength + buffer.Length;
			Label2.Text = totalLength.ToString();
		}

 

       Method GetMessageDataLength

       在 DoReceive Method (程式碼在後面) 將會使用 SerialPort.Read 方法將接收緩衝區的資料讀回來,並將它一個 Byte 一個 Byte  放在 List<Byte> 中,這個方法就是用來取得長度碼以解析在 List <Byte> 中倒底哪一段資料是完整的一段。

 

	Private Function GetMessageDataLength(ByVal tempList As List(Of Byte)) As Int32
		If tempList.Count >= 2 Then
			Dim startIndex As Int32 = tempList.IndexOf(head)
			If startIndex >= 0 AndAlso startIndex < tempList.Count Then
				Return Convert.ToInt32(tempList(startIndex + 1))
			Else
				Return 0
			End If
		Else
			Return 0
		End If
	End Function

 

 


		private Int32 GetMessageDataLength(List<Byte> tempList)
		{
			if (tempList.Count >= 2)
			{
				Int32 startIndex = tempList.IndexOf(head);
				if (startIndex >= 0 && startIndex < tempList.Count )
				{
					return Convert.ToInt32(tempList[startIndex + 1]);
				}
				else
				{ return 0; }
			}
			else
			{ return 0; }
		}

   

       這段程式的主要邏輯是先判斷 List<Byte> tempList 中的資料長度是否大於等於 2,因為長度碼是第二個元素,如果 tempList 中只有一個 Byte 那表示不用判斷了;再來要取得在 tempList 中是否有開頭碼存在,因為有可能在 tempList 中是 (資料)(資料)(開頭碼)(長度碼)(資料),所以要先確認出開頭碼的位置,而它的下一個元素才會是長度碼。至於為何要加上 startIndex < tempList.Count 的判斷是因為如果它是最後一個元素,你硬要取下一個就會發生例外。

 

       Parse Method

       這個方法則是去擷取出一整段完整的資料內容,然後呼叫 DisplayText 來顯示在 TextBox 中。

 


	Private Function Parse(ByRef tempList As List(Of Byte), ByVal messageDataLength As Int32) As Int32
		If tempList.Count >= messageDataLength Then
			Dim tempArray(messageDataLength - 1) As Byte
			Dim startIndex As Int32 = tempList.IndexOf(head)
			If startIndex >= 0 Then
				tempList.CopyTo(startIndex + 2, tempArray, 0, messageDataLength)
				tempList.RemoveRange(0, startIndex + messageDataLength + 2)
				messageDataLength = GetMessageDataLength(tempList)
				Dim d As New Display(AddressOf DisplayText)
				Me.Invoke(d, New Object() {tempArray})
			End If
		End If
		Return messageDataLength
	End Function

 

 


		private Int32 Parse(List<Byte> tempList, Int32 messageDataLength)
		{
			if (tempList.Count >= messageDataLength)
			{
				Byte[] tempArray = new Byte[messageDataLength];
				Int32 startIndex = tempList.IndexOf(head);
				if (startIndex >= 0)
				{
					tempList.CopyTo(startIndex + 2, tempArray, 0, messageDataLength);
					tempList.RemoveRange(0, startIndex + messageDataLength + 2);
					messageDataLength = GetMessageDataLength(tempList);
					Display d = new Display(DisplayText);
					this.Invoke(d, new Object[] { tempArray });
				}
			}
			return messageDataLength;
		}

 

       這個方法中使用了 [List(T).CopyTo 方法 (Int32, T[], Int32, Int32)]將一段的資料內容 (不含開頭碼與長度碼) 複製到 tempArray 這個 Byte 陣列中。這也就是為什麼 CopyTo 的第一個參數值要加 2 的原因,如果你搞得不太懂,拿出紙筆來畫一下相對位置關係就知道了。當我們複製好資料內容後,接著就是將資料使用 [List(T).RemoveRange 方法 ] 移出 tempList;一樣的道理,如果不清楚相對位置,用紙筆畫一畫就曉得了。然後再呼叫 GetMessageDataLength 取得下一段的長度並將此值返回。

 

      Method DoReceive

       最後來完成接收的主體部份。


	Private Sub DoReceive()
		Dim tempList As New List(Of Byte)
		Dim buffer(1023) As Byte
		Dim messageDataLength As Int32 = 0
		Dim ticks As Int32 = 0
		While receiving = True
			Thread.Sleep(100)
			If comport.BytesToRead > 0 Then
				Dim receivedLength As Int32 = comport.Read(buffer, 0, buffer.Length)
				Array.Resize(buffer, receivedLength)
				tempList.AddRange(buffer)
				Array.Resize(buffer, 1024)
			End If
			If tempList.Count > 0 Then
				If messageDataLength = 0 Then
					messageDataLength = GetMessageDataLength(tempList)
				Else
					messageDataLength = Parse(tempList, messageDataLength)
				End If
			End If
		End While
	End Sub

 


		private void DoReceive()
		{
			List<Byte> tempList = new List<Byte>();
			Byte[] buffer = new Byte[1024];
			Int32 messageDataLength = 0;
			while (receiving)
			{
				Thread.Sleep(100);
				if (comport.BytesToRead > 0)
				{
					Int32 receivedLength = comport.Read(buffer, 0, buffer.Length);
					Array.Resize(ref buffer, receivedLength);
					tempList.AddRange(buffer);
					Array.Resize(ref buffer, 1024);
				}
				if (tempList.Count > 0)
				{
					if (messageDataLength == 0)
					{
						messageDataLength = GetMessageDataLength(tempList);
					}
					else
					{
						messageDataLength = Parse(tempList, messageDataLength);
					}
				}
			}
		}

       主體部份的原則是這樣,如果緩衝區有資料就讀回來後用 [List(T).AddRange 方法] 存到 tempList 中,如果 tempList 有資料就以目前的 messageDataLength 值來決定是要取得資料長度還是要取得資料內容並顯示。

 

       整個程式大概就是這樣,當然這不是唯一的寫法,遞迴也可以做,而且可以做出另外一個效果,就是當你的 tempList 中的資料不只一段的時候可以一直遞迴呼叫先處理擷取資料並顯示它的部份直到 tempList 中沒有完整的資料為止,這個就留給大家自行發揮。另一個是不知道你們有沒有注意到上一篇是用 ReadByte 方法;而這篇卻是用 Read 方法,基本上這兩個狀況都可以用這兩種不同的方法來處理,差異只是邏輯判斷上的變化,所以你也可以試著把上一篇改成用 Read 來寫,而這一篇用 ReadByte 來寫,就自個兒發揮想像力了吧。