在工业领域,RS485和虚拟USB串口仍然扮演着重要的角色。无论是与数控机床或其他工业机械交换数据,还是通过Modbus与暖通空调设备或器具进行接口,串行通信虽然隐蔽但无处不在。
在设计SCADA系统或图形面板的用户界面时,确保为用户提供正确的视觉更新至关重要,即使在长时间的数据交换会话中,或者在执行耗时操作时也是如此。标签、图片框、仪表和其他控件需要在后台不断更新。
同事们经常要求的一个功能是,通过串行线的视觉表示RX/TX数据,因为大多数便宜的串行适配器和接口没有数据LED灯。
本文将展示如何使用后台工作者更新两个图片框的UI,模仿红色和绿色LED灯。当然,这个概念可以扩展到所有需要异步更新的视觉控件。
将从设置表单开始。对于这个示例,只需要一个包含两个图片框的表单,一个用于TX LED,另一个用于RX LED。将它们的可见性设置为False,并调整它们的大小,以适当地模仿表单左上角的两个LED。将它们命名为pbSend(TX)和pbReceive(RX)。
UI更新必须在主线程中进行,因为后台工作者在主线程之外的线程上运行,将设置一个委托来为执行此操作。
还将声明后台工作者本身,一个用于TX LED,另一个用于RX LED。
Private WithEvents sp As New SerialPort
Private Delegate Sub LEDSwitchDelegate(ByRef a As PictureBox, ByVal b As Boolean)
Private WithEvents backgroundWorker1 As New BackgroundWorker()
Private WithEvents backgroundWorker2 As New BackgroundWorker()
实际提供切换两个LED开和关的子程序如下。将通过传递图片框名称和其可见性作为参数来调用它。例如:
LEDSwitch(pbSend, True)
这将导致pbSend LED被切换为ON。这将在主线程和任何其他线程上工作,多亏了委托!
Private Sub LEDSwitch(ByRef a As PictureBox, ByVal b As Boolean)
If Me.InvokeRequired Then
Dim d As New LEDSwitchDelegate(AddressOf Me.LEDSwitch)
Me.BeginInvoke(d, New Object() {a, b})
Else
Try
a.Visible = b
Catch ex As Exception
End Try
End If
End Sub
在项目中,LED灯将在发送或接收一个字节或多个字节时切换为ON,持续500毫秒,因此后台工作者将运行确切的时间。
Private Sub backgroundWorker1_DoWork(ByVal sender As Object, ByVal e As DoWorkEventArgs) Handles backgroundWorker1.DoWork
Thread.Sleep(500)
End Sub
Private Sub backgroundWorker2_DoWork(ByVal sender As Object, ByVal e As DoWorkEventArgs) Handles backgroundWorker2.DoWork
Thread.Sleep(500)
End Sub
他们不会做太多事情,除了等待500毫秒(这里里程可能会有所不同,可能会更好地利用后台工作者浪费的周期)。
工作完成后(只是一个500毫秒的非阻塞小睡),LED灯将再次关闭。将使用与两个后台工作者中的每一个相关联的RunWorkerCompleted事件。
Private Sub backgroundWorker1_RunWorkerCompleted(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs) Handles backgroundWorker1.RunWorkerCompleted
LEDSwitch(pbSend, False)
End Sub
Private Sub backgroundWorker2_RunWorkerCompleted(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs) Handles backgroundWorker2.RunWorkerCompleted
LEDSwitch(pbReceive, False)
End Sub
需要一个专用的子程序来切换LED灯,例如当数据被接收时。将使用一个子程序来实现这一点,并将子程序绑定到Serialport类实例的DataReceived事件。因为TransmittingData()在主线程上,可以直接切换LED灯。确保后台工作者不忙,以避免多次调用它们。
Private Sub TransmittingData()
LEDSwitch(pbSend, True)
If Not backgroundWorker1.IsBusy Then
' Start the asynchronous operation.
backgroundWorker1.RunWorkerAsync()
End If
End Sub
Private Sub ReceivingData()
LEDSwitch(pbReceive, True)
If Not backgroundWorker2.IsBusy Then
' Start the asynchronous operation.
backgroundWorker2.RunWorkerAsync()
End If
End Sub
需要的最后一个子程序就是form_Load方法,在其中添加DataReceived事件的处理程序,并将其绑定到ReceivingData()。
Private Sub AsynchPictureboxes_Load(sender As Object, e As EventArgs) Handles MyBase.Load
AddHandler sp.DataReceived, AddressOf ReceivingData
End Sub
不幸的是,System.IO.Ports.SerialPort类没有DataSent事件,所以不能直接绑定它,但这很容易克服,因为每当需要传输一个字节或多个字节时,可能已经有一个子程序来处理它。将使用调用Serialport.Write方法的子程序来调用TransmittingData()子程序。
如所见,不需要担心切换LED灯的关闭。后台工作者会照顾到这一点。
可以扩展方法:例如,可以显示和识别端口或数据错误,例如,两个LED灯同时快速闪烁。在这种情况下,可以按照以下方式绑定事件,并编写特定的DataError方法来处理这种情况。
AddHandler sp.ErrorReceived, AddressOf DataError
可以开发进一步的方法来显示,例如,CRC,校验和错误等。
LED灯闪烁的频率可以通过程序更改,通过将适当的脉冲时间传递给后台工作者,以响应特定的消息或数据报。
一个或多个组合框和文本框可以由后台工作者以程序方式填充,例如,实时地与串行通信中传入的数据一起,而不会占用主线程并使其无响应。