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

在進入『發送接收模型』的議題之前,先來討論關於接收資料完整性的問題,在我之前的文章中都是接收緩衝區有多少資料就收多少回來,這產生一個問題是我們常常會遇到分段接收的狀況,也就是說傳送端一次傳送了一份完整的資料,但接收端卻未必會一次就收的完,這樣不完整接收的現象不僅在 SerialPort 會發生,在 Socket 也一樣會產生這種狀況,所以必須採用一些技巧來確認資料的完整性。不過這有非常多不同的情境,在這幾篇文章中會舉出幾項例子但請注意未必能完全符合各種情境的需要。

       在進入『發送接收模型』的議題之前,先來討論關於接收資料完整性的問題,在我之前的文章中都是接收緩衝區有多少資料就收多少回來,這產生一個問題是我們常常會遇到分段接收的狀況,也就是說傳送端一次傳送了一份完整的資料,但接收端卻未必會一次就收的完,這樣不完整接收的現象不僅在 SerialPort 會發生,在 Socket 也一樣會產生這種狀況,所以必須採用一些技巧來確認資料的完整性。不過這有非常多不同的情境,在這幾篇文章中會舉出幾項例子但請注意未必能完全符合各種情境的需要。

 

       最簡易也最基本的方式

       我們都知道資料處理與傳送需要時間,所以最便捷的方法就是等資料全都送到緩衝區後再一次讀取,在 SerialPort.Read 之前應用 Thread.Sleep 方法就可以達到這樣的目的,但究竟應該要睡多久就得測測看了,因為每個設備的反應時間不一,甚至同一個設備處理不同的命令的所需時間也可能不同,但這是一個很基本的方式,建議大家在讀取緩衝區資料之前都放上這個程序。

 

       接下來就是根據通訊協定的一些情境來說明各狀況的解決方式。

 

       情境一:固定長度資料

       這情境其實是我想像的,如果有個通訊協定是固定長度沒有其它條件,其實毛病可能會挺多的,不過不排除有可能會有這樣的情形,面對這樣的狀況最簡單的方式就是使用等待迴圈檢查接收緩衝區的大小,並且固定其接收長度。以下的範例示範其長度固定為 1024 Bytes 的接收方式:(由 [Serial Port 系列(11) 基本篇 -- 利用執行緒讀取資料] 的範例程式修改而來)


	Private Sub DoReceive()
		Dim buffer(1023) As Byte
		While receiving = True
			While comport.BytesToRead < 1024
				Thread.Sleep(16)
			End While
			Dim length As Int32 = comport.Read(buffer, 0, buffer.Length)
			Dim d As New Display(AddressOf DisplayText)
			Me.Invoke(d, New Object() {buffer})
			Thread.Sleep(16)
		End While
	End Sub

 

 


		private void DoReceive()
		{
			Byte[] buffer = new Byte[1024];
			while (receiving)
			{
				while (comport.BytesToRead < 1024)
				{
					Thread.Sleep(16);
				}
				Int32 length = comport.Read(buffer, 0, buffer.Length);
				Display d = new Display(DisplayText);
				this.Invoke(d, new Object[] { buffer });
				Thread.Sleep(16);				
			}
		}

       這個等待迴圈檢查的重點在於當接收緩衝區內資料不滿 1024 Bytes 時則不斷地運行迴圈,並在迴圈內使用 Thread.Sleep 稍做等待,你不一定要用 16ms,也可以設定的更長一些,端看你測試出來哪一個時間設定最有效果。

 

       但是以上的範例是建立在『正常讀取』的情況,以上述例子來說如果在緩衝區收到 700 Bytes 時卻斷線怎麼辦?程式豈不就在While迴圈內繞個不停?因此程式內要加上一些其它必要的判斷來避免這樣的問題,例如限定時間或是次數超過就跳出等待迴圈。

 

       限定次數

       使用限定次數的原則在於你必須先計算好容許的次數,這個條件值和回應時間、資料長度與Baud Rate相關連,有一種不幸的可能是把限制條件設的太低以致於永遠讀不到資料,以下的例子以 500 次當做一個基準,事實上正常狀況下在我的電腦用以下程式碼完整讀取 1024 Bytes 的次數約莫落在 45~70 次之間 (通常我會用最大範圍乘以2以保安全)。這個範例中多宣告了兩個變數:

       (1) Int32 型別的 count 變數:這個變數在計算當緩衝區有資料但資料量小於1024 Bytes 時在等待迴圈的次數,並且本身也是等待迴圈的條件,當 count 值大於 501 時不論接收緩衝區的資料量是否大於1024 依然跳出等待迴圈。

       (2) Boolean型別的 readingFromBuffer 變數:若是因為讀取不完全而離開等待迴圈則將此變數值設為Fasle;程序離開等待迴圈後依據此變數值執行不同的狀況。

 

       此範例的幾個技巧在於:

       (1) 先檢查接收緩衝區中是否有資料,若是有資料才進行等待迴圈。

 

       (2) 當一旦檢查到緩衝區內有資料進入時先使用 Thread.Sleep 讓執行緒停止一段較長的時間,通常等待的時間越長,在等待迴圈中的次數就會越少,以這個例子而言,當我設定Thread.Sleep(1200)後幾乎是完全不會進入等待迴圈。

 

       (3) 等待迴圈的條件除了檢查接收緩衝區資料量外,同時也檢查等待迴圈的次數, 若是跳離迴圈的原因是因為次數超過則將readingFromBuffer 變數值設為False。

 

       (4) 跳離等待迴圈後請將 count 變數值重置為0。

 

       (5) 當readingFromBuffer 變數值為True表示接收緩衝區內的資料量已達目標,因此使用 SerialPort.Read 讀取資料;若為False則表示資料緩衝區內的資料不足,在範例中的做法是用 [SerialPort.DiscardInBuffer 方法] 直接清除接收緩衝區,若你要做更精細的處理,請依狀況自行調整此區域程式碼。

 


	Private Sub DoReceive()
		Dim readingFromBuffer As Boolean
		Dim count As Int32 = 0
		Dim buffer(1023) As Byte
		While receiving = True
			If comport.BytesToRead > 0 Then
				Thread.Sleep(312)
				readingFromBuffer = True
				While (comport.BytesToRead < buffer.Length AndAlso count < 501)
					Thread.Sleep(16)
					count += 1
					If count > 500 Then
						readingFromBuffer = False
					End If
				End While
				count = 0
				If readingFromBuffer = True Then
					Dim length As Int32 = comport.Read(buffer, 0, buffer.Length)
					Dim d As New Display(AddressOf DisplayText)
					Me.Invoke(d, New Object() {buffer})
				Else
					comport.DiscardInBuffer()
				End If
			End If
			Thread.Sleep(16)
		End While
	End Sub

 


		private void DoReceive()
		{
			Boolean readingFromBuffer;
			Int32 count = 0;
			Byte[] buffer = new Byte[1024];
			while (receiving)
			{
				readingFromBuffer = true;
				while (comport.BytesToRead < buffer.Length && count < 501)
				{
					Thread.Sleep(16);
					count++;
					if (count > 500)
					{
						readingFromBuffer = false;
					}
				}
				count = 0;
				if (readingFromBuffer)
				{
					Int32 length = comport.Read(buffer, 0, buffer.Length);
					Display d = new Display(DisplayText);
					this.Invoke(d, new Object[] { buffer });
				}
				else
				{
					comport.DiscardInBuffer();
				}

				Thread.Sleep(16);				
			}
		}

 

       限定時間

       限定時間的技巧其實和限定次數差不多,只是改成用 [Stopwatch 類別] 來測量等待的時間而已。

 

       (1) 產生一個 Stopwatch 類別的執行個體,並且在檢查接收緩衝區有資料的狀況下呼叫 [Stopwatch.Start 方法] 啟動Stopwatch。

 

       (2) 等待迴圈的條件加入 Stopwatch的耗用時間,若耗用時間大於 3000 Milliseconds時跳離等待迴圈則將readingFromBuffer 變數值設為False。

 

       (3) 離開等待迴圈後呼叫 [Stopwatch.Stop 方法] 停止測量,再呼叫 [Stopwatch.Reset 方法] 將計數歸零。


	Private Sub DoReceive()
		Dim readingFromBuffer As Boolean
		Dim watch As Stopwatch = New Stopwatch()
		Dim buffer(1023) As Byte
		While receiving = True
			If comport.BytesToRead > 0 Then
				Thread.Sleep(312)
				watch.Start()
				readingFromBuffer = True
				While (comport.BytesToRead < buffer.Length AndAlso watch.ElapsedMilliseconds < 3001)
					Thread.Sleep(16)
					If watch.ElapsedMilliseconds > 3000 Then
						readingFromBuffer = False
					End If
				End While
				watch.Stop()
				watch.Reset()
				If readingFromBuffer = True Then
					Dim length As Int32 = comport.Read(buffer, 0, buffer.Length)
					Dim d As New Display(AddressOf DisplayText)
					Me.Invoke(d, New Object() {buffer})
				Else
					comport.DiscardInBuffer()
				End If
			End If
			Thread.Sleep(16)
		End While
	End Sub

 


		private void DoReceive()
		{
			Boolean readingFromBuffer;
			Stopwatch watch = new Stopwatch();
			Byte[] buffer = new Byte[1024];
			while (receiving)
			{
				if (comport.BytesToRead > 0)
				{
					Thread.Sleep(312);
					watch.Start();
					readingFromBuffer = true;
					while (comport.BytesToRead < buffer.Length && watch.ElapsedMilliseconds < 3001)
					{
						Thread.Sleep(16);
						if (watch.ElapsedMilliseconds > 3000)
						{
							readingFromBuffer = false;
						}
					}
					watch.Stop();
					watch.Reset();
					if (readingFromBuffer)
					{
						Int32 length = comport.Read(buffer, 0, buffer.Length);
						Display d = new Display(DisplayText);
						this.Invoke(d, new Object[] { buffer });
					}
					else
					{
						comport.DiscardInBuffer();
					}
				}
				Thread.Sleep(16);			
			}
		}

 

       限定次數或時間的技巧在其它情境也都可以應用的上,上述的範例如果把 readingFromBuffer 變數改成屬性的話程式可以變得更有趣,這就留待大伙兒自己發揮創意去改造吧。