Getting Started with Go
Initializing a dummy project
Just in case you want to get started with Go
. In this part we’ll setup a new Go
project.
If you are familiar with this, you can go to the next chapter.
Be sure to have installed Go
on your system by downloading and installing it from https://golang.org/.
Now that that’s done, create a new directory and console execute the following command:
go mod init main
This will effectively only create a go.mod
file, which is sort of like Go’s counterpart to a Maven pom.xml.
This should look like this:
module main
go 1.16
Now we need to create a app.go
file which will be our main program.
Create a file app.go
with the following content:
package main func main() { }
By executing the command:
go run app.go
You will execute your first Go
progran … however the output is rather underwhelming ;-)
You’re now ready to continue.
Using the PLC4Go API directly
In order to write a valid PLC4X Go application, all you need, is to add a dependency to the plc4go module
.
Now all you need to do, is execute the following command:
go get github.com/apache/plc4x/plc4go
This will checkout the latest version of the Apache PLC4X PLC4Go
module.
As soon as we have released a PLC4Go version by adding the name of the release-tag will use an explicit version.
This will be checked out in our home directory in
⁓/go/pkg/mod/github.com/apache/plc4/plc4go@v0.0.0-{some-commit-hash}
In contrast to the PLC4J version this already contains all supported drivers. Perhaps we’ll change this in the future, but for now all comes in one bundle.
Now you’re generally set to start writing your first PLC4Go
program.
Connecting to a PLC
In contrast to PLC4J, which uses the service lookup to find the transports
and the drivers
automatically, in PLC4Go
they need to be manually registered at the driver manager.
First we need to initialize the PlcDriverManager
by registering the transports and drivers with it.
// 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.RegisterModbusDriver(driverManager)
Now that the PlcDriverManager
is configured, we can use it to get a new connection.
// Get a connection to a remote PLC connectionRequestChanel := driverManager.GetConnection("modbus-tcp://192.168.23.30?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()
In PLC4Go we make heavy use of Go channels
, which are similar to Futures
or Promisses
.
And please pay attention to the defer
command.
This adds a call to a stack of things that need to be called as soon as the program terminates.
However in contrast to Java’s try-finally
blocks, this isn’t executed at the end of the code-block, but really when the program terminates.
So when working with many connections or when using connections in loops (if for example you are polling), then this will keep on piling up active connections, till either you are no longer able to connect cause your PLC denies connections or till you run out of memory.
So if you only need the connection in a code block, be sure to explicitly close it after usage.
After this code block we should be in possession of a connection
instance.
If we simply want to check the connectivity, we can use the Ping
function on the connection object.
Depending on the protocol used, it will exeute a command which only will complete if the connection is available.
// 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 }
Reading Data
Most probably you will want to read something from a PLC.
This is done by a PlcReadRequest
.
First off all, it’s probably a good idea to check if this connection supports reading:
if !connection.GetMetadata().CanRead() { fmt.Printf("This connection doesn't support read operations") return }
In order to create and run such a PlcReadRequest
, please add the following code:
// 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 }
// 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 }
If you have any errors in the addresses or whatever, you will get an err
instead of a readRequest
.
For now, let’s assume you got all addresses correctly.
// 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 }
Please note that in this case we want to return a triple: PlcReadRequest
, PlcReadResponse
, err
.
As this is not supported in Go
, the PlcReadRequestResult
will contain all of these 3 elements.
This will probably change soon. The API is still a bit in flux. |
Now in order to do something with the response:
// 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())
The GetValue
function returns a PlcValue
instance, this had accessors for the most general Go
types.
Subscribing to Data
As the Modbus
protocol, which we used in the above examples, doesn’t support subscriptions, we are using the KNX
protocol for a demonstration on the subscription API.
Subscribing to data can be considered similar to reading data, at least the subscription itself if very similar to reading of data.
We first have to check if the connection supports this:
if !connection.GetMetadata().CanSubscribe() { fmt.Printf("This connection doesn't support subscriptions operations") return }
Now we’ll create the subscription request.
The main difference is that while reading there is only one form how you could read, with subscriptions there are different forms of subscriptions:
-
Change of state (Event is sent as soon as a value changes)
-
Cyclic (The Event is sent in regular cyclic intervals)
-
Event (The Event is usually explicitly sent form the PLC as a signal)
Therefore instead of using a normal AddItem
, there are tree different functions as you can see in the following examples.
// 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 }
// 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 }
The Event hadnler
for intercepting incoming events could look like this:
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()) } } }
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()) } } }
The AddCyclicField /AddCyclicTagAddress method requires a third parameter duration which specifies the interval, in which a given value is sent (even if it has not changed).
|
Here the API differs slightly form the Java version, as in the request-builder itself you specify the reference to the callback handler which should be notified on incoming data. However, we will be aligning all API variants as much as possible in the near future. |
The request itself is executed exactly the same way the read and write operations are executed, using the Execute
function.
// 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 }