Go 快速入门
初始化一个虚拟项目
以防你希望从 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
}评论
登录后参与评论
KnowForge