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.
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.
{
"name": "temperature",
"equipment": "line1",
"type": "analogical",
"store": true,
"driver": {
"type": "analogical",
"plc": "line1",
"symbol": "MAIN.fMachineTemperature",
"acquisition": "notify"
}
}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:
{
"name": "start",
"equipment": "line1",
"driver": {
"type": "discrete",
"value": 1,
"plc": "line1",
"symbol": "MAIN.bStart"
}
}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:
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: 10sads 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 (48898by default).source_net_id— the AMS Net ID the agent uses, e.g.192.168.1.20.1.1.source_portdefaults to32750, andtimeout(connect and every request) to5s.plcs— the PLC runtimes reached through this connection, keyed by theplcname used in every driver above. Each has atarget_net_idand atarget_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 oftenpolland failednotify_or_pollmeasurements are sum-read. Omit it only if every measurement usesnotify(the default) — apollmeasurement never updates without apoll_intervalto drive it.notif_cycle_time— how often the PLC checks a notified symbol for a change (100msby default).notif_max_delay— how long the PLC may batch changes before sending them (0sby default).comms_lost_after— how long a PLC must stay unreachable before its measurements are republished with bad quality (30sby 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.
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.