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

具備開頭結尾字元
有一些通訊協的形式是具備了開頭結尾字元形式的,當然不一定是一個字,不過通常的情形是開頭或結尾字元絕不會出現在中間的資料內容段中,以下的圖例是以大寫字母S做為開頭字元;而大寫E字母做為結尾字元的通訊協定形式。

       情境二:具備開頭結尾字元

       有一些通訊協的形式是具備了開頭結尾字元形式的,當然不一定是一個字,不過通常的情形是開頭或結尾字元絕不會出現在中間的資料內容段中,以下的圖例是以大寫字母S做為開頭字元;而大寫E字母做為結尾字元的通訊協定形式。

Protocol_SE

 

       純接收的狀況:

       這其實會有幾種不同的適用方式,如果是純接收的狀態以 [Serial Port 系列(11) 基本篇 -- 利用執行緒讀取資料] 的範例來接收的話,我們有可能會這樣收到資料:

       第一次:S9987

       第二次:4366ES02

       第三次:75543298E

 

       這樣的情形着實讓人覺得很困擾,不過它有一個最簡單的解決方法 -- 一個Byte 一個Byte 讀取,這會應用到另一個SerialPort的內建方法 [SerialPort.ReadByte 方法] (其實用 SerialPort.Read 方法 (Byte[], Int32, Int32) 也行,把第三個參數設成1就好了), ReadByte  和  Read  在回傳值的定義是不同的, ReadByte  回傳的值就是你從緩衝區讀回來的那個 Byte 值,而且它的型別還不是 Byte 而是 Int32,這原因是因為它要定義一個資料流末端的值,這個值是 -1,所以它才會變成需要使用 Int32 而非 Byte 型別。

 

       為了要測試 ReadByte 方法,先來建立一個發送的模擬程式,這是程式是由 [Serial Port 系列(7) 基本篇 -- 建立一個簡單的純發送程式(多緒型範例)]  改編其 SendData 方法而來,以下僅列出改變的部份:

 

	Private Sub SendData(ByVal port As Object)
		Dim buffer(1023) As Byte
		For j = 0 To 2
			Dim length As Int32
			Select Case j
				Case 0
					length = 256
				Case 1
					length = 512
				Case 2
					length = 1024
			End Select
			Array.Resize(buffer, length)
			buffer(0) = Encoding.ASCII.GetBytes("S")(0)
			buffer(buffer.Length - 1) = Encoding.ASCII.GetBytes("E")(0)
			For i As Int32 = 1 To Buffer.Length - 2
				buffer(i) = Encoding.ASCII.GetBytes((i Mod 10).ToString())(0)
			Next
			sending = True
			Try
				DirectCast(port, SerialPort).Write(Buffer, 0, Buffer.Length)
			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 = new Byte[1024];
			for (int j = 0; j < 3; j++)
			{
				Int32 length = 0;
				switch (j)
				{
					case 0:
						length = 256;
						break;
					case 1:
						length = 512;
						break;
					case 2:
						length = 1024;
						break;
					default:
						break;
				}

				Array.Resize(ref buffer, length);
				buffer[0] = Encoding.ASCII.GetBytes("S")[0];
				buffer[buffer.Length - 1] = Encoding.ASCII.GetBytes("E")[0];
				for (int i = 1; i <= buffer.Length - 2; i++)
				{
					buffer[i] = Encoding.ASCII.GetBytes((i % 10).ToString())[0];
				}
				sending = true;
				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;
				}
			}
		}

 

       為了製造出連續發送的效果,在外圈多包了一個迴圈,讓它連送三次,資料的內容為:

       第一次:S+ 254 Bytes(1~0 的數字) + E

       第二次:S+ 510 Bytes(1~0 的數字) + E

       第三次:S+ 1022 Bytes(1~0 的數字) + E

 

       如果你使用在 [Serial Port 系列(11) 基本篇 -- 利用執行緒讀取資料] 的原有程式做為接收方,你就會發現在前述提到那種亂接一通的困擾,接著我們就來看看如何用 ReadByte 方法來處理這樣的問題。

 

       接著來修改[Serial Port 系列(11) 基本篇 -- 利用執行緒讀取資料] 中的某些方法:

 

       Method DoReceive

       首先在方法內宣告一個 List(Of Byte) / List<Byte> 型別的變數 tempList,將每次讀到的值先指派給receiveValue 再存放在這個變數所指向的執行個體;在存放進 tempList 前我們必須先該 receivedValue 值的判斷以便處理不同的情況。當讀到的值與大寫S (也就是開頭字元) 的 Byte 值相同時則先清除 tempList 的內容 (這表示在S之前的資料都不要了) 再將 S 的值加入 tempList;當讀到的值與大寫E (也就是結尾字元) 的 Byte 值相同時則先加入 E 的值到 tempList 然後呼叫自訂的 parse 方法處理;若是讀到 –1 則不管它,若是讀到並非以上的值時則直接加入到 tempList (這有一個假設是讀到的值都是可以轉成可見字元的,實際上為安全起見應該要再加上此一判斷才對)。

 

 

	Private Sub DoReceive()
		Dim tempList As New List(Of Byte)
		While receiving = True
			Dim receivedValue As Int32 = comport.ReadByte()
			Select Case receivedValue
				Case Encoding.ASCII.GetBytes("S")(0)
					tempList.Clear()
					tempList.Add(CType(receivedValue, Byte))
				Case Encoding.ASCII.GetBytes("E")(0)
					tempList.Add(CType(receivedValue, Byte))
					parse(tempList)
				Case -1
					'do Nothing
				Case Else
					tempList.Add(CType(receivedValue, Byte))
			End Select
		End While
	End Sub

 

       由於C# 的 switch case 無法像 Viusal Basic 這樣瞎弄,所以在 C# 的程式碼中我多宣告了兩個類別內的全域常數。

		const Int32 S = 83;
		const Int32 E = 69;

		private void DoReceive()
		{
			List<Byte> tempList = new List<Byte>();

			while (receiving)
			{
				Int32 receivedValue = comport.ReadByte();
				switch (receivedValue)
				{
					case S:
						tempList.Clear();
						tempList.Add((Byte)receivedValue);
						break;
					case E:
						tempList.Add((Byte)receivedValue);
						parse(tempList);
						break;
					case -1:
						break;
					default:
						tempList.Add((Byte)receivedValue);
						break;
				}
			}
		}

 

       Method Parse

       這個方法主要是在收到結尾字元後 (因為表示已經收到一段完整資料了) 呼叫,由於我們只要顯示資料內容的部份,所以在這方法中把開頭結尾字元給去除。

	Private Sub parse(ByVal tempList As List(Of Byte))
		If tempList(0) = Encoding.ASCII.GetBytes("S")(0) AndAlso tempList(tempList.Count - 1) = Encoding.ASCII.GetBytes("E")(0) Then
			tempList.RemoveAt(0)
			tempList.RemoveAt(tempList.Count - 1)
			Dim d As New Display(AddressOf DisplayText)
			Me.Invoke(d, New Object() {tempList.ToArray()})
		End If
	End Sub

 

		private void parse(List<Byte> tempList)
		{
			MessageBox.Show(tempList[0].ToString());
			if (tempList[0] == (Byte)S && tempList[tempList.Count - 1] == (Byte)E)
			{
				tempList.RemoveAt(0);
				tempList.RemoveAt(tempList.Count - 1);
				Display d = new Display(DisplayText);
				this.Invoke(d, new Object[] { tempList.ToArray() });

			}			
		}

 

       Method DisplayText

       最後一個步驟是來改裝一下 DisplayText 方法,利用 [Encoding.GetString 方法 (Byte[])]使其可以顯示的是字元的內容而非 Byte 值。

	Private Sub DisplayText(ByVal buffer As Byte())
		TextBox1.Text &= String.Format("{0}{1}", Encoding.ASCII.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.ASCII.GetString(buffer), Environment.NewLine);
			totalLength = totalLength + buffer.Length;
			Label2.Text = totalLength.ToString();
		}

 

       這篇介紹的是具備可辨視開頭結尾字元純接收的最基本技巧,實際狀況可能會衍生出更多的判斷與處理需求。當然這篇的假設情形你也可以用 Read 方法來做讀取,但判斷的過程就會更複雜一些。

 

      【備註1】 這篇文章用到了一些 [Syste.Text.Encoding 類別] 轉換 ASCII 字碼與其 Byte 值的技巧,如果你不熟悉這個類別的使用,請詳閱 MSDN 文件庫。

      【備註2】在接收的部份使用了 [List(T) 類別],對此不熟練的話也請詳閱 MSDN 文件庫。

      【備註3】其實一直 Encoding 來 Encoding 去的還滿煩的,尤其真實狀況要判別的命令可能很多的時候,實際上你可以採用其它方法來替代,比方用列舉、自訂靜態屬性、自訂常數 (像是上面 C# Code的用法) 等等方式來處理,這留給大家自行想像。