Finding ADS devices on the network, and adding routes to them, over UDP port 48899, as TwinCAT's own Broadcast Search and Add Route do:
{:ok, [%{address: {10, 0, 0, 5}, net_id: "5.36.69.176.1.1"}]} = ADS.Discovery.search()
{:ok, %{net_id: "5.36.69.176.1.1"}} = ADS.Discovery.info("10.0.0.5")
:ok = ADS.Discovery.add_route("10.0.0.5", password: "1")A PLC lets in only clients it has a route for: their AMS NetId, and the address they come from.
add_route/2 adds one with the login of a user on the PLC, Administrator unless told otherwise,
for the NetId the client is to use, by default its address with .1.1 as ADS.Client takes
it, so that a client can connect after without TwinCAT's tools. The password goes over the
network as it is, as with TwinCAT's tools; Secure ADS adds routes inside TLS instead (see
ADS.Secure).
Beckhoff's manuals say only that discovery goes over UDP port 48899. What goes in it follows Beckhoff's own ADS library (github.com/Beckhoff/ADS, MIT License), which asks a device for its NetId over it, and adds routes; deleting them follows Apache PLC4X's description of the protocol (plc4x.apache.org, Apache License 2.0). Each datagram, request or answer, is:
| Bytes | Field |
|---|---|
| 4 | 0x71146603 |
| 4 | an invoke id, 0, which the answer has too |
| 4 | the service: 1 for a device's info, 6 to add a route, 7 to delete one; with 0x80000000 added in the answer |
| 6, 2 | an AMS NetId and port, the sender's: in a request to add a route, the route's NetId |
| 4 | the number of tags |
| ... | the tags, each a 2-byte id, the length of its value in 2 bytes, and the value |
To add or delete a route, the tags are the route's name (12), its NetId (7), the user (13), the password (2) and the client's address (5), the strings with a zero after them, in Windows-1252 here as in Secure ADS; the answer's tag 1 is its result, a return code of 4 bytes. All numbers are little-endian.
An answer is taken only from the address asked, with the service and invoke id of the request; anything else that comes is passed over. Nothing sent is ever empty, which older TwinCAT routers stopped at (Beckhoff's advisory 2019-004).
Summary
Types
A device that answered: its IP address, AMS NetId, and the tags of its answer, {id, value},
and of those, as TwinCAT 3.1 4026 has them
Functions
Adds a route on a device, for a client to connect with.
A datagram that came, or :error if it isn't one. Its tags are read as far as they go, and as
many as it says it has, at most.
Deletes a route on a device, with the login of a user on it, as add_route/2 adds one and with
the same options. TwinCAT 3.1 4026 deletes the route of the name, by default the address, as
add_route/2 names it: give the name it was added with. Another route with the same NetId stays.
A datagram as bytes to send.
Asks one device for its info: its AMS NetId above all.
Asks every device on the network for its info, and gives those that answer in time, in the
order they did. A firewall on this machine that lets in only answers to what was sent, as ufw
does, drops theirs, as they come from another address than the broadcast went to; info/2
asks one device by its address, and its answer gets through.
Types
@type datagram() :: %{ :service => 0..4_294_967_295, optional(:invoke_id) => 0..4_294_967_295, net_id: ADS.NetId.t(), port: ADS.ams_port(), tags: [{0..65535, binary()}] }
A datagram, as encode/1 takes it, its invoke id 0 if left out, and decode/1 gives it.
@type device() :: %{ address: :inet.ip4_address(), net_id: ADS.net_id(), name: String.t() | nil, version: {byte(), byte(), 0..65535} | nil, fingerprint: String.t() | nil, tags: [{0..65535, binary()}] }
A device that answered: its IP address, AMS NetId, and the tags of its answer, {id, value},
and of those, as TwinCAT 3.1 4026 has them:
name- its name, tag 5, the tag of a computer's name in a routeversion- TwinCAT's, tag 3, such as{3, 1, 4026}fingerprint- the SHA-256 of its certificate for Secure ADS, tag 18, in hex as TwinCAT shows it andsecure: [fingerprint: ...]takes it. It comes over UDP, which anyone on the network can answer: compare it with the one TwinCAT shows before trusting it.
Each is nil if the device doesn't give it.
Functions
@spec add_route(String.t() | :inet.ip4_address(), keyword()) :: :ok | {:error, term()}
Adds a route on a device, for a client to connect with.
:password- of the user on the device (required):user- (default"Administrator"):net_id- the client's NetId, as it's to start withlocal_net_id:if it's not the default: its address with.1.1:address- the client's address, as the device is to know it (default the address it sends from):name- the route's name (default the address):port- the UDP port (default 48899):timeout- in ms (default 5000):codepage- of the strings (default Windows-1252; seeADS.Codepage)
:ok once the device has added the route, or {:error, reason}: an ADS return code if it
wouldn't, such as for a wrong password, or :timeout.
A datagram that came, or :error if it isn't one. Its tags are read as far as they go, and as
many as it says it has, at most.
@spec delete_route(String.t() | :inet.ip4_address(), keyword()) :: :ok | {:error, term()}
Deletes a route on a device, with the login of a user on it, as add_route/2 adds one and with
the same options. TwinCAT 3.1 4026 deletes the route of the name, by default the address, as
add_route/2 names it: give the name it was added with. Another route with the same NetId stays.
:ok once the device has deleted it, or {:error, reason} as for add_route/2:
:device_not_found from Beckhoff's own system service for a route it hasn't. Beckhoff's router
lets a connection that's open stay, its route deleted. TwinCAT 3.1 4026's answer doesn't tell
whether there was such a route: it has answered :ok, and :device_error, to deleting one it
hadn't. So mix ads.route --delete sees that the device answers this machine over ADS before,
and not after, on a new connection.
A datagram as bytes to send.
@spec info(String.t() | :inet.ip4_address(), keyword()) :: {:ok, device()} | {:error, term()}
Asks one device for its info: its AMS NetId above all.
:port- the UDP port (default 48899):timeout- in ms (default 5000):codepage- of its name (default Windows-1252; seeADS.Codepage)
@spec search(keyword()) :: {:ok, [device()]} | {:error, :inet.posix()}
Asks every device on the network for its info, and gives those that answer in time, in the
order they did. A firewall on this machine that lets in only answers to what was sent, as ufw
does, drops theirs, as they come from another address than the broadcast went to; info/2
asks one device by its address, and its answer gets through.
:to- the addresses to ask, broadcast or not (default{255, 255, 255, 255}, and the broadcast address of each network this machine is on):port- the UDP port (default 48899):timeout- how long to wait for answers, in ms (default 2000):codepage- of the devices' names (default Windows-1252; seeADS.Codepage)