Beckhoff ADS with golang

The Beckhoff PLC exposes FunctionBlocks and other objects via the ADS protocol which is spoken over TCP. This article describes how to send and receive ADS requests and responses on a very basic level with golang.

I am a golang beginner so the code will not be anywhere near production grade. The code should show the very basics of the ADS protocol and the structure of the messages.

ADS Monitor

The ADS Monitor can be installed into the TwinCAT IDE and can be opened using the TwinCAT menu and the ADS Monitor menu item.

It will dissect packages. Also it contains a filter functionality. The filtering only works when the package capture is stopped! You cannot apply the filter to ongoing traffic. To apply the filter, first stop capturing. This yields a set of logged packages after stopping capturing. You then have to click on the “Apply” Button! The filter contained in the “Display Filter” edit field is then applied to the log of packets.

A sample filter is ams.sendernetid == 192.168.0.234.1.1

ADS Read State Request

One of the simplest requests to send is to ask the ADS Device (= main device in the PLC) for it’s current state. It will answer with NO_ERROR in the happy-case.

The request can be sent with a invoke id of zero (0), it requires no login or specific handshake or anything else. It is a very simple example for a Request-Response pattern.

Here is what the request looks like in the ADS monitor:

Here is what the response looks like in the ADS monitor:

The golang code to send and receive the request and response above is here:

import (
"encoding/hex"
"flag"
"fmt"
"net"
"strings"
)

func main() {

fmt.Println("Connecting ...")
conn, err := net.Dial("tcp", "192.168.0.108:48898")
if err != nil {
fmt.Println("Error occured ", err)
return
}
fmt.Println("Connecting done.")

fmt.Println("Building request ...")
requestAsHexString := "00 00 20 00 00 00 c0 a8 f7 21 01 01 10 27 c0 a8 00 ea 01 01 ee 7f 04 00 04 00 00 00 00 00 00 00 00 00 00 00 00 00"
buffer, err := hex.DecodeString(strings.ReplaceAll(requestAsHexString, " ", ""))
if err != nil {
fmt.Println("Building request failed ", err)
return
}
fmt.Println("Building request done.")

fmt.Println("Writing ...")
bytesWritten, err := conn.Write(buffer)
if err != nil {
fmt.Println("Write failed ", err)
return
}
fmt.Println("Writing done. Bytes written: ", bytesWritten)

fmt.Println("Reading ...")
recvBuffer := make([]byte, 1024)
bytesRead, err := conn.Read(recvBuffer)
if err != nil {
fmt.Println("Read failed ", err)
return
}
fmt.Println("Reading done. Bytes read: ", bytesRead, " data: %s", recvBuffer)
ExampleEncode(recvBuffer)

defer conn.Close()

}

func ExampleEncode(src []byte) {

dst := make([]byte, hex.EncodedLen(len(src)))
hex.Encode(dst, src)

fmt.Printf("%s\n", dst)

// Output:
// 48656c6c6f20476f7068657221
}

The code has to be improved a lot. Datatype definitions have to be added for AMS- and ADS-headers. The response has to be parsed. There is no handling for the response. The code is not thread-safe, the list goes on.

The only thing this code highlights is the fact that communicating with the ADS protocol is done by sending byte buffers over a tcp port! That is a success and opens up a lot of possibilities.

Structure of the Request and the Response

The byte buffers that are sent and received are not just random byte buffers. The structure is clearly defined in the ADS protocol so that each byte buffer can be interpreted and parsed correctly.

The Automation Device Specification (ADS) data packet is wrapped inside a Automated Message Specification (AMS) data packet.

The overall packet structure (structure of the request and response bytes arrays) looks like this: There is a AMS/TCP header, followed by a AMS Header followed by ADS data. ADS data is optional, a package does not have to send ADS data if the request is fully specified without additional information for example.

Checkout the Beckhoff information system for an overview over the exact fields inside each part of the packets.

Dissecting the request from the example program according to the Beckhoff ADS specifiction yields:

-- ADS TCP Header
00 00 20 00 00 00

-- AMS Header
c0 a8 f7 21 01 01 - AMS NetId Target - 192.168.247.33.1.1
10 27 - AMS Port Target
c0 a8 00 ea 01 01 - AMS NetId Source - 192.168.0.234.1.1
ee 7f - AMS Port Source - 61055
04 00 - Command Id - (0 - Invalid, 1 - ReadDeviceInfo,
2 - Read, 3 - Write, 4 - ReadState,
5 - WriteControl,
...)
04 00 - State Flags
00 00 00 00 - Length
00 00 00 00 - Error Code
00 00 00 00 - Invoke Id

-- ADS Read State Request
- Data (not present, not needed here)

Dissecting the response yields:

-- ADS TCP Header
00 00 28 00 00 00

-- AMS Header
c0 a8 00 02 01 01 - AMS NetId Target - 192.168.0.2.1.1
ee 7f - AMS Port Target
c0 a8 f7 21 01 01 - AMS NetId Source - 192 168 247 33 1 1
10 27 - AMS Port Source - 61055
04 00 - Command Id - (0 - Invalid, 1 - ReadDeviceInfo,
2 - Read, 3 - Write, 4 - ReadState,
5 - WriteControl,
...)
05 00 - State Flags
08 00 00 00 - Length
00 00 00 00 - Error Code
00 00 00 00 - Invoke Id

-- ADS Read State Response
00 00 00 00 - Result (0 == NO ERROR)
05 00 - AdsState (5 == ADS State)
01 00 - DeviceState (1 == Device State)

Leave a Reply