快速入门

Go 快速入门

qianmoQqianmoQ· 更新于 2026-10-01· 阅读 18 分钟· 0 次阅读

登录后可跨设备保存划线和私人笔记登录

初始化一个虚拟项目

以防你希望从 Go 开始上手。在本部分中,我们将创建一个新的 Go 项目。如果你已经熟悉这些操作,可以直接跳到下一章。

请确保已通过从 https://golang.org/ 下载并安装了 Go。

完成后,创建一个新目录,并在控制台中执行以下命令:

go mod init main

这实际上只会创建一个 go.mod 文件,它有点类似于 Go 版本的 Maven pom.xml。

结果应该如下所示:

module main

go 1.16

现在我们需要创建一个 app.go 文件,它将作为我们的主程序。

创建一个 app.go 文件,内容如下:

package main

func main() {

}

通过执行命令:

go run app.go

你将执行你的第一个 Go 程序……不过输出效果相当平淡 ;-)

现在你可以继续了。

直接使用 PLC4Go API

要编写一个有效的 PLC4X Go 应用程序,你只需要添加对 plc4go module 的依赖。现在你要做的就是执行以下命令:

go get github.com/apache/plc4x/plc4go

这将检出最新版本的 Apache PLC4X PLC4Go 模块。一旦我们通过添加发布标签的名称发布了 PLC4Go 版本,就会使用指定的版本。

这将在我们的主目录中检出,位于

⁓/go/pkg/mod/github.com/apache/plc4/plc4go@v0.0.0-{some-commit-hash}

与 PLC4J 版本不同,这里已经包含了所有受支持的驱动。将来我们或许会改变这一点,但目前所有内容都打包在一个 bundle 中。

至此,你已经基本准备好开始编写第一个 PLC4Go 程序了。

连接到 PLC

与 PLC4J 使用服务查找机制自动发现 transports 和 drivers 不同,在 PLC4Go 中需要手动将它们注册到驱动管理器。

首先,我们需要通过向 PlcDriverManager 注册传输器和驱动来对其进行初始化。

    // Create a new instance of the PlcDriverManager
    driverManager := plc4go.NewPlcDriverManager()

    // Register the Transports
    transports.RegisterTcpTransport(driverManager)
    transports.RegisterUdpTransport(driverManager)

    // Register the Drivers
    drivers.RegisterKnxDriver(driverManager)
    drivers.RegisterModbusTcpDriver(driverManager)

每种 Modbus 变体都单独注册:RegisterModbusTcpDriver、RegisterModbusRtuDriver 和 RegisterModbusAsciiDriver。这两种串行变体使用各自的帧格式,并自行注册串行传输层,因此请选择与你所通信设备相匹配的那一种。

现在 PlcDriverManager 已经配置完毕,我们可以用它来获取一个新连接。

       // Get a connection to a remote PLC
    connectionRequestChanel := driverManager.GetConnection("modbus-tcp://192.168.23.30?default-unit-identifier=1")

    // Wait for the driver to connect (or not)
    connectionResult := <-connectionRequestChanel

    // Check if something went wrong
    if connectionResult.Err != nil {
        fmt.Printf("Error connecting to PLC: %s", connectionResult.Err.Error())
        return
    }

    // If all was ok, get the connection instance
    connection := connectionResult.Connection

    // Make sure the connection is closed at the end
    defer connection.Close()

在 PLC4Go 中,我们大量使用 Go 的 channels,它们与 Futures 或 Promisses 类似。

同时请注意 defer 命令。它会将一次调用加入到一个调用栈中,这些调用会在程序终止时被立即执行。然而,与 Java 的 try-finally 块不同,它并不是在代码块结束时执行,而是在程序真正终止时才执行。因此,如果处理大量连接,或者在循环中使用连接(例如进行轮询),活动连接会不断累积,直到你的 PLC 拒绝新的连接导致无法再建立连接,或者直到内存耗尽。

所以,如果你只需要在某个代码块中使用连接,务必在使用后显式关闭它。

经过这段代码之后,我们应该已经持有了一个 connection 实例。

如果我们只是想检查连接的连通性,可以在连接对象上使用 Ping 函数。根据所使用的协议不同,它会执行一条只有在连接可用时才会完成的命令。

    // Try to ping the remote device
    pingResultChannel := connection.Ping()

    // Wait for the Ping operation to finsh
    pingResult := <-pingResultChannel
    if pingResult.Err != nil {
        fmt.Printf("Couldn't ping device: %s", pingResult.Err.Error())
        return
    }

读取数据

你很可能是想从 PLC 中读取某些数据。这可以通过一次 PlcReadRequest 来完成。

首先,最好先检查一下该连接是否支持读取:

    if !connection.GetMetadata().CanRead() {
        fmt.Printf("This connection doesn't support read operations")
        return
    }

为了创建并运行这样一个 PlcReadRequest,请添加以下代码:

截至 0.10.0 版本

    // Prepare a read-request
    readRequest, err := connection.ReadRequestBuilder().
        AddQuery("field1", "holding-register:1:REAL").
        AddQuery("field2", "holding-register:3:REAL").
        Build()
    if err != nil {
        t.Errorf("error preparing read-request: %s", connectionResult.Err.Error())
        t.Fail()
        return
    }

SNAPSHOT 版本

    // Prepare a read-request
    readRequest, err := connection.ReadRequestBuilder().
        AddTagAddress("tag1", "holding-register:1:REAL").
        AddTagAddress("tag2", "holding-register:3:REAL").
        Build()
    if err != nil {
        t.Errorf("error preparing read-request: %s", connectionResult.Err.Error())
        t.Fail()
        return
    }

如果你在地址或其他方面有误,你得到的将是 err 而不是 readRequest。

目前,我们假设你所有的地址都填写正确。

    // Execute a read-request
    readResponseChanel := readRequest.Execute()

    // Wait for the response to finish
    readRequestResult := <-readResponseChanel
    if readRequestResult.Err != nil {
        t.Errorf("error executing read-request: %s", readRequestResult.Err.Error())
        return
    }

请注意,在本例中我们要返回一个三元组:PlcReadRequest、PlcReadResponse、err。由于 Go 不支持这种形式,PlcReadRequestResult 将包含这 3 个元素。

这一点可能很快就会改变。该 API 目前仍有些变动。

现在,为了对响应做进一步处理:

    // Do something with the response
    value1 := readRequestResult.Response.GetValue("field1")
    value2 := readRequestResult.Response.GetValue("field2")
    fmt.Printf("\n\nResult field1: %f\n", value1.GetFloat32())
    fmt.Printf("\n\nResult field2: %f\n", value2.GetFloat32())

GetValue 函数返回一个 PlcValue 实例,它提供了最通用的 Go 类型的访问器。

写入数据

尚未实现

订阅数据

由于我们在上述示例中使用的 Modbus 协议不支持订阅,因此我们使用 KNX 协议来演示订阅 API。

订阅数据可以被视为与读取数据类似,至少订阅本身与读取数据非常相似。

我们必须首先检查连接是否支持该功能:

    if !connection.GetMetadata().CanSubscribe() {
        fmt.Printf("This connection doesn't support subscriptions operations")
        return
    }

现在我们来创建订阅请求。

主要区别在于:读取时只有一种读取方式,而订阅则有不同类型:

  • 状态变更(当值发生变化时立即发送事件)
  • 周期(按固定的时间间隔发送事件)
  • 事件(通常由 PLC 显式发送,作为一个信号)

因此,这里不再使用普通的 AddItem,而是有三个不同的函数,你可以在以下示例中看到。

截至 0.10.0 版本

    // Prepare a subscription-request
    subscriptionRequest, err := connection.SubscriptionRequestBuilder().
        AddChangeOfStateItem("heating-actual-temperature", "*/*/10:DPT_Value_Temp").
        AddChangeOfStateItem("heating-target-temperature", "*/*/11:DPT_Value_Temp").
        AddCyclicItem("heating-valve-open", "*/*/12:DPT_OpenClose", 500 * time.Millisecond).
        AddItemHandler(knxEventHandler).
        Build()
    if err != nil {
        fmt.Printf("Error preparing subscription-request: %s", connectionResult.Err.Error())
        return
    }

SNAPSHOT 版本

    // Prepare a subscription-request
    subscriptionRequest, err := connection.SubscriptionRequestBuilder().
        AddChangeOfStateTagAddress("heating-actual-temperature", "*/*/10:DPT_Value_Temp").
        AddChangeOfStateTagAddress("heating-target-temperature", "*/*/11:DPT_Value_Temp").
        AddCyclicTagAddress("heating-valve-open", "*/*/12:DPT_OpenClose", 500 * time.Millisecond).
        AddItemHandler(knxEventHandler).
        Build()
    if err != nil {
        fmt.Printf("Error preparing subscription-request: %s", connectionResult.Err.Error())
        return
    }

拦截传入事件的 Event hadnler 可以这样编写:

截至 0.10.0 版本

func knxEventHandler(event apiModel.PlcSubscriptionEvent) {
    for _, fieldName := range event.GetFieldNames() {
        if event.GetResponseCode(fieldName) == apiModel.PlcResponseCode_OK {
            groupAddress := event.GetAddress(fieldName)
            fmt.Printf("Got update for field %s with address %s. Value changed to: %s\n",
                fieldName, groupAddress, event.GetValue(fieldName).GetString())
        }
    }
}

SNAPSHOT 版本

func knxEventHandler(event apiModel.PlcSubscriptionEvent) {
    for _, tagName := range event.GetTagNames() {
        if event.GetResponseCode(tagName) == apiModel.PlcResponseCode_OK {
            groupAddress := event.GetTag(tagName).GetAddressString()
            fmt.Printf("Got update for tag %s with address %s. Value changed to: %s\n",
                tagName, groupAddress, event.GetValue(tagName).GetString())
        }
    }
}
AddCyclicField/AddCyclicTagAddress 方法需要第三个参数 duration,用于指定发送给定值的时间间隔(即使该值并未发生变化)。
这里 API 与 Java 版本略有不同,因为需要在请求构建器本身中指定回调处理器的引用,以便在收到数据时通知该处理器。不过,我们将在不久的将来尽可能统一所有 API 变体。

请求本身的执行方式与读写操作完全相同,均使用 Execute 函数。

    // Execute a subscription-request
    subscriptionRequestResultChanel := subscriptionRequest.Execute()

    // Wait for the response to finish
    subscriptionRequestResult := <-subscriptionRequestResultChanel
    if subscriptionRequestResult.Err != nil {
        fmt.Printf("Error executing read-request: %s", subscriptionRequestResult.Err.Error())
        return
    }

评论

登录后参与评论

正在加载评论…