65.9K
CodeProject 正在变化。 阅读更多。
Home

用于硬件设备通信的 DevCom 框架

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2010年11月13日

CPOL

9分钟阅读

viewsIcon

24804

downloadIcon

561

DevCom 框架的描述和示例用法。

引言

这是一个通用的、可扩展的框架,涵盖了硬件设备控制的大多数通信方面。它将数据与设备之间的数据格式解析和传输抽象为一个易于使用的对象模型。不再需要重写所有与协议和技术相关的代码。

背景

许多硬件设备(主要是使用的控制器)仍然通过串行通信连接到外部世界。串行通信成本低且可靠。随着桥接 IC 的易于获取,可以桥接到 USB、以太网等,因此几乎不需要更改(至少对于低带宽应用程序而言)。使用这些 IC,可以轻松创建具有不同连接方式的同一设备的多个版本。在构建远程控制这些硬件的应用程序时,您需要实现其硬件控制协议。但您还需要支持每种不同的连接方法。

我用 Lua 脚本语言为 Girder(一个家庭自动化应用程序)编写了一些驱动程序。Girder 有一个抽象层,消除了所有与连接相关的细节。现在我正在为我的家庭自动化迁移到 xPL,目前我正在用 Visual Basic 进行开发。在重复 reinvent 了同样的事情之后,构建一个类似的、可扩展的抽象层似乎是个好主意。它应该能够处理多种技术(RS232、TCP/IP 等)的通信以及一些基本的消息协议。设备特定的控制协议通常基于同一种消息协议,固定长度数据、带有前缀长度字节的数据或带有终止序列的数据是最常见的。这些也应该得到支持并且易于扩展。

模型解释

主要结构是 Transport 对象。它使用了 4 个主要接口

  1. IFormatter
  2. ITransportLayer (及其伴随的 ISettings
  3. IParser
  4. IMessage

下图展示了模型以及它与宿主应用程序和被处理设备的关系,并对每个组件进行了简要描述。

IFormatter

将一个对象格式化为一个 IMessage ,该消息可以通过 ITransportLayer 传输到设备。可用的格式化器有

  • none:仅传递字节而不进行格式化
  • terminated:在数据后添加一个终止序列
  • fixed length:在适当的时候用填充字节填充剩余部分
  • length byte:在命令前加上一个长度字节

ITransportLayer

处理实际通信。它通过有线(或无线)将命令传输到设备,并监听传入的数据以传递给 IParser。每种技术都应该有一个特定的 transportlayer。目前只有 TCP/IP(以及用于测试目的的 Echo transportlayer ),RS232 紧随其后。transportlayer 必须始终伴随一个 ISettings 对象,该对象能够配置 transportlayer 并包含用户界面。

传输层使用自己的发送和接收队列,这些队列在单独的线程上处理。这简化了 transportlayer 的开发,因为它可以使用阻塞调用(同步)而不是更复杂的线程调用(异步),而不会阻塞父应用程序的执行。

IParser

解析器执行的操作与 IFormatter 完全相反,并具有相同的标准解析器。因此,从远程设备(通过 transportlayer)接收到的任何字节都将被解析为一个有效的响应。响应可以与发送的原始命令重新组合,因此传输层传递给宿主应用程序的数据包括原始命令及其响应(当然,如果收到数据但没有之前的命令,则只有响应数据可用)。

IMessage

包含要发送的数据,并将保存接收到的响应。它还包括用于处理附加功能的处理程序。通过为不同的命令创建单独的对象,可以将响应解析为命令特定的属性,这些属性可以由宿主应用程序轻松处理。

使用代码:示例

第一个可用的(仍然粗糙且有 bug 的)版本。让我们通过一些代码来演练一个示例。

假设我们有以下情况;一个使用 xHCP 协议的 xPL HAL 服务器。它是基于 xPL 的家庭自动化设置的逻辑引擎。该协议是基于文本的,并具有以下特征:

  • 通信是 TCP,端口为 3865
  • 消息以 2 个字节终止;十六进制 0D 0A
  • 命令是一个单独的命令字,后跟可选参数
  • 响应前面有一个 3 位数的返回码,后面跟着文本描述
  • 多行消息以单独一行的“.”终止

让我们实现一个基本的传输层,并让它在 xPL-HAL 服务器上执行 SETGLOBAL 命令。这将设置其脚本引擎中的一个全局变量到一个特定值。

我将使用一个小巧的 Windows Forms 应用程序来设置变量及其值,来演示该库。

步骤 1:构建和配置传输层

Private WithEvents tsp As Transport

Private Sub MainForm_Load(ByVal sender As System.Object, _
	ByVal e As System.EventArgs) Handles MyBase.Load
    Dim ts As TCPIPSettings
    Dim es As EchoSettings

    ' create transport object
    tsp = New Transport

    ' Create and add the TCP/IP connection
    ts = New TCPIPSettings(tsp)
    ts.Host = "testsystem.local.lan"
    ts.PortNum = 3865
    tsp.SettingsAdd(ts)

    ' Make TCP/IP settings the active set
    tsp.ActiveSettings(ts)

    ' Create and add the Echo layer
    es = New EchoSettings(tsp)
    tsp.SettingsAdd(es)

    ' Ask user for connection preferences
    tsp.SettingsShow()

    ' Set message defaults, terminated by hex 0D 0A
    Dim arrLineTerminator As Byte() = {13, 10}
    tsp.SetFormatter(New FormatterTerminated(arrLineTerminator))
    tsp.SetParser(New ParserTerminated(arrLineTerminator))

    ' start the show
    tsp.Initialize()
    Try
        tsp.Open()
    Catch ex As Exception
        MsgBox("Error while opening the transport: " & ex.Message)
        Me.Close()
    End Try

End Sub

代码在 tsp 变量中创建一个 Transport。然后,它添加了 2 个设置对象;TCP/IP 和 Echo(Echo 仅用于演示目的,RS232 会更有意义,但尚未提供)。通过调用其 AddSettings 方法将它们都添加到 Transport 中。活动设置以及 TCP/IP 的默认设置(设置 hostname portnumber)也已设置。

接下来是显示连接配置对话框,该对话框允许用户配置要使用的连接。

配置对话框在下拉列表中显示可用的连接,用户可以在其中选择要使用的连接。协议设置窗格作为 UserControl 通过 settingsobject 提供。一旦用户在此对话框中选择了连接方法,宿主应用程序就不需要知道传输层实际使用了什么连接。

设置过程的最后一步是添加 Formatter Parser Transport 使用。对于 xPL-HAL 服务器,这是一个以十六进制 0D 0A 终止的协议。因此,它们就是这样设置的。

添加 Parser Formatter 后,Transport 被初始化然后打开。当 transport 打开连接时,它会调用 ActiveSettings 对象并请求一个 TransportLayer (设置对象创建并配置 TransportLayer ,然后将其交给 Transport)。

简而言之

  1. 将设备支持的每个层/连接添加到传输设置中
  2. 向用户显示配置对话框
  3. 添加 Formatter Parser
  4. 打开连接

步骤 2:接收消息

当窗体显示时,连接已由传输层打开,xPL-HAL 服务器会响应一个状态消息,列出其 xPL 地址和版本信息。这是初始窗口

处理传入数据的代码是 Transport 的 eventhandler

Private Sub DataReceived(ByVal sender As Transport, ByVal data As IMessage) _
	Handles tsp.DataReceived
    ' Eventhandler for received responses
    Dim d As SetResponseDelegate
    ' cast returned data to byte array and convert to a string
    Dim resp As String = Utility.encBytes2String(CType(data.ResponseData, Byte()))
    ' Invoke method to display the async returned response
    d = AddressOf SetResponse
    If Me.InvokeRequired Then
        Me.Invoke(d, resp)
    Else
        SetResponse(resp)
    End If
End Sub
Delegate Sub SetResponseDelegate(ByVal response As String)
Private Sub SetResponse(ByVal response As String)
    tbResponse.Text = response
End Sub

通过 IMessage.ResponseData 属性接收到的数据被格式化为 string ,然后显示在 tbReponse textbox 中(通过委托,因为传入的数据在不同的线程上)。

步骤 3:发送命令

发送数据同样直接。示例命令 SETGLOBAL 有两个参数,名称和值,用空格分隔。为了在用户单击发送时处理该命令

Private Sub btnSend_Click(ByVal sender As System.Object, _
	ByVal e As System.EventArgs) Handles btnSend.Click
    Dim t As String
    ' Setup command string
    t = "SETGLOBAL " & tbVarName.Text & " " & tbValue.Text
    ' Convert to byte array and send
    tsp.Send(Utility.encString2Bytes(t))
End Sub

创建命令 string,将其转换为字节数组,然后调用 Transport.Send。如果您单击示例中的“发送”按钮,响应将立即显示

(2xx 的响应代码表示成功。)

这就是传输层实现通信的简单方式。请记住,到目前为止我们还没有看到套接字,也没有看到 COM 端口或线程池和队列处理器。所有这些都由 Transport 负责。但在查看命令和响应的处理时,我们发送一个命令然后等待响应,然后我们必须进行匹配,此外我们仍然得到一个通用的字节数组来消化。这也可以由传输层来处理。让我们来看一个更高级的示例...

另一个示例:完整的命令-响应周期

无论何时传输命令,最方便的是在返回时将命令的响应链接起来。为此,我们可以为我们的命令创建一个特定的 IMessage 实现,并让它重构接收到的响应。在本例中,我将引入 4 个属性;全局变量名、其值、响应码、响应消息。这是命令实现(基类 BasicMessage 是库中包含的一个简单的 IMessage 实现);

Friend Class cmdSetGlobal
    Inherits BasicMessage

    Public GlobalName As String = ""            ' name of the global variable to be set
    Public GlobalValue As String = ""           ' value to be set
    Public ReturnCode As Integer                ' xPL-HAL response code
    Public ReturnMessage As String              ' xPL-HAL response message
    Public Const SuccesCode As Integer = 232    ' ReturnCode that indicates success

    Public Sub New(Optional ByVal strGlobalName As String = "", _
		Optional ByVal strGlobalValue As String = "")
        Me.Name = "SetGlobal"
        Me.Description = "Set a value in global variable of the xPL-HAL scripting engine"
        Me.GlobalName = strGlobalName
        Me.GlobalValue = strGlobalValue
        Me.RequiresResponse = True

        Dim arrLineTerminator As Byte() = {13, 10}
        _parser = New ParserTerminated(arrLineTerminator)
    End Sub

    Private _parser As ParserTerminated
    Public Overrides Function GetParser(ByVal sender As Tieske.DevCom.Transport) _
	As Tieske.DevCom.IParser
        Return _parser
    End Function

    Public Overrides Property MessageData() As Object
        Get
            ' return data constructed from the command and the properties values as set
            Return Utility.encString2Bytes("SETGLOBAL " & Me.GlobalName & _
		" " & Me.GlobalValue)
        End Get
        Set(ByVal value As Object)
            ' do nothing
        End Set
    End Property
    Public Overrides Sub OnResponseReceived_
	(ByVal sender As Tieske.DevCom.Transport, ByVal data As Object)
        MyBase.OnResponseReceived(sender, data)
        ' Method runs when a received response has been successfully parsed
        ' Extract the values from the response received
        Dim msg As String = Utility.encBytes2String(CType(data, Byte()))
        Me.ReturnCode = CInt(Val(Microsoft.VisualBasic.Left(msg, 4)))
        Me.ReturnMessage = Mid(msg, 5)
    End Sub
End Class

跳过显而易见的部分,专注于有趣的细节。

新的构造函数是直接的,但它有两条重要的行

  1. 'Me.RequiresResponse = True' 告诉 Transport,在发送命令后,它不能释放 IMessage 对象,但必须将其保留在队列中等待正确的响应。
  2. '_parser = New ParserTerminated(arrLineTerminator)' 设置了此消息要使用的特定解析器。在这种情况下,它与主解析器(在 Transport 对象本身中设置)相同,但并非必须如此。

接收数据时,接收队列(依次)被传递给所有仍在等待响应的消息,按照先入先出的顺序(队列中最长的消息将首先获得数据)。如果所有解析器都失败,传入的数据将被传递给 Transport 的通用 Parser

IMessage.MessageData 属性是存储命令数据的地方,因此在此属性中,根据先前介绍的属性构建实际命令。类似地,当数据成功解析该消息后,会调用 IMessage.OnDataReceived 方法,因此我们在此处从返回的字节数组中提取响应属性的数据。

现在已经创建了一个特定的命令对象,发送命令变得很简单,只需创建一个新对象并发送它;

Private Sub btnSend_Click(ByVal sender As System.Object, _
	ByVal e As System.EventArgs) Handles btnSend.Click
    ' Create a command class and send it
    tsp.Send(New cmdSetGlobal(tbVarName.Text, tbValue.Text))
End Sub

在接收部分,我们现在可以简单地检查对象类型(请记住,接收到的对象与发送的对象是同一个实例,在前面的示例中会返回一个新对象)。只有当它是对发送命令的响应时,它才会被处理。xPL-HAL 服务器的通用欢迎消息将不会通过此处,因此它不会像第一个示例那样显示。这是接收的 eventhandler

Private Sub DataReceived(ByVal sender As Transport, _
	ByVal data As IMessage) Handles tsp.DataReceived
    ' Eventhandler for received responses
    Dim d As SetResponseDelegate

    ' If received data is not a response to the cmdSetGlobal, then exit
    If Not TypeOf data Is cmdSetGlobal Then Exit Sub

    ' Invoke method to display the async returned response
    d = AddressOf SetResponse
    If Me.InvokeRequired Then
        Me.Invoke(d, CType(data, cmdSetGlobal))
    Else
        SetResponse(CType(data, cmdSetGlobal))
    End If
End Sub

显示方法(SetResponse)现在接受一个 cmdSetGlobal 作为参数,并根据自定义属性的具体内容设置显示属性。这是代码

Private Sub SetResponse(ByVal msg As cmdSetGlobal)
    tbResponse.Text = msg.ReturnMessage
    tbResponseCode.Text = msg.ReturnCode.ToString
    If msg.ReturnCode = cmdSetGlobal.SuccesCode Then
        ' success
        tbResponseCode.BackColor = Color.Green
    Else
        ' failure
        tbResponseCode.BackColor = Color.Red
    End If
End Sub

这使其看起来像这样

关注点

即使是这些相当简单的例子,其潜力也是显而易见的。第一个示例已经很好地封装了所有通信方面的代码,无论您使用什么技术。第二个示例更进一步,它不仅仅以某种通用形式传递数据,而是以针对其使用应用程序/设备的特定方式传递数据。

由于整个系统由多个接口组成,因此易于扩展。添加一个 transportlayerparser formatter ,它们就可以被重复使用。

从这里开始...

该项目离生产就绪还有很长的路要走。源代码可在 SourceForge 上找到,一个项目正在进行中。非常欢迎评论和贡献。

待办事项列表

  • 添加 RS232(COM 端口)传输层
  • 添加心跳信号
  • 添加连接控制(连接失败时自动重启)
  • 加强异常处理(目前非常薄弱)

历史

  • 2010 年 11 月 12 日 硬件竞赛的初始文章
© . All rights reserved.