Guide

TwinCAT ADS

Reads and writes Beckhoff TwinCAT PLC symbols over ADS, addressed by PLC name plus symbol path — device notifications preferred over polling.

This is the protocol reference for the TwinCAT ADS agent. For everything else in the config format, see Agent configuration.

01
Driver address

plc and symbol together are the join key (plc::symbol), so the same symbol on two different PLCs doesn't collide. plc is a name you choose — it must match a PLC key in the agent's own config (its docker image config, below), which is what actually holds the connection. symbol is the PLC symbol path, e.g. MAIN.fMachineTemperature.

TwinCAT resolves the whole path on the PLC, so array elements and struct fields go straight in the path — MAIN.aValues[3], MAIN.stMotor.rSpeed, even MAIN.pValue^.nValue through a pointer. Point at a single value: a path that resolves to a whole struct, array, or string has no single numeric reading, and the agent drops it. Booleans read as 0/1, and an enum reads (and writes) as its numeric value.

acquisition picks how the value is read, defaulting to notify:

  • notify — only an ADS device notification (on change): the PLC pushes the value whenever it changes.
  • notify_or_poll — prefer a notification; fall back to polling if the subscription fails.
  • poll — always poll (batched into one sum-read), even when a notification would be available.
measurement
{
  "name": "temperature",
  "equipment": "line1",
  "type": "analogical",
  "store": true,
  "driver": {
    "type": "analogical",
    "plc": "line1",
    "symbol": "MAIN.fMachineTemperature",
    "acquisition": "notify"
  }
}
Commands

Same plc and symbol, this time naming the symbol to write (e.g. MAIN.bStart). A command is a typed write of that symbol — a discrete command writes its fixed value, an analogical one writes the value sent with the request. ADS has no select-before-operate, so there's no control model to pick; use an interlock if the write needs a condition first:

command
{
  "name": "start",
  "equipment": "line1",
  "driver": {
    "type": "discrete",
    "value": 1,
    "plc": "line1",
    "symbol": "MAIN.bStart"
  }
}
02
Docker image

Image: khorde/ads-agent:latest. It doesn't serve an API — the only thing on 9090 is a liveness check, GET /health, which returns all_connected plus a connected flag per PLC — same compose shape as every agent (see Agent configuration). Save the agent's service account key next to it, renamed to service-user.json. Its own config.yaml:

config.yaml
ads:
  - address: <ams-router-ip>
    port: 48898
    source_net_id: <agent-ams-net-id>
    plcs:
      line1:
        target_net_id: <plc-ams-net-id>
        target_port: 851
      line2:
        target_net_id: <plc-ams-net-id>
        target_port: 852
poll_interval: 5s
agent:
  broker:
    ip: host.docker.internal
    port: "4222"
    tls: false
  credentials:
    url: http://auth.localhost:9310
    service_account_path: service-user.json
  command_timeout: 10s

ads is a list of connections, one per AMS router (usually the TwinCAT device itself). The agent talks to each router directly over TCP — no local AMS router needed — and opens every connection on startup, whether or not a measurement or command references it yet.

  • address / port— the router's host and AMS TCP port (48898 by default).
  • source_net_id — the AMS Net ID the agent uses, e.g. 192.168.1.20.1.1. source_port defaults to 32750, and timeout (connect and every request) to 5s.
  • plcs — the PLC runtimes reached through this connection, keyed by the plc name used in every driver above. Each has a target_net_id and a target_port (851, the first TwinCAT 3 runtime, by default). Names must be unique across the whole file, not just within one connection.

ADS routes each frame by its target address, so one connection can serve several PLCs — other devices behind the same router, or several runtimes (851, 852, …) on the same device. List every PLC behind the same router under a single connection: a second connection to that router from the same source_net_id tears down the first.

Add a route on the PLC. Each router needs a static route back to the agent — add one in TwinCAT (or StaticRoutes.xml) with the agent's source_net_id as the AMS Net ID and the IP address the PLC sees the agent connect from. Without it, the connection opens but every ADS request times out.

  • poll_interval — how often poll and failed notify_or_poll measurements are sum-read. Omit it only if every measurement uses notify (the default) — a poll measurement never updates without a poll_interval to drive it.
  • notif_cycle_time — how often the PLC checks a notified symbol for a change (100ms by default). notif_max_delay — how long the PLC may batch changes before sending them (0s by default).
  • comms_lost_after — how long a PLC must stay unreachable before its measurements are republished with bad quality (30s by default). Today this marks every measurement on the agent bad, not just the ones on the PLC that dropped.

A few more advanced settings — heartbeat_interval and reconnect backoff (reconnect_min_delay, reconnect_max_delay) — are optional too; the defaults work for a first connection.

Next

Agent configuration

The full schema shared by every protocol. Agent configuration.

Other protocols

REST, IEC 60870-5-104, and IEC 61850 agents work the same way.

Taking this past your laptop?

Talk to us