ADS.Discovery (yaads v0.1.0)

Copy Markdown View Source

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:

BytesField
40x71146603
4an invoke id, 0, which the answer has too
4the service: 1 for a device's info, 6 to add a route, 7 to delete one; with 0x80000000 added in the answer
6, 2an AMS NetId and port, the sender's: in a request to add a route, the route's NetId
4the 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 datagram, as encode/1 takes it, its invoke id 0 if left out, and decode/1 gives it.

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

datagram()

@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.

device()

@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 route
  • version - 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 and secure: [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

add_route(host, opts)

@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 with local_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; see ADS.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.

decode(arg1)

@spec decode(binary()) :: {:ok, datagram()} | :error

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.

delete_route(host, opts)

@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.

encode(datagram)

@spec encode(datagram()) :: binary()

A datagram as bytes to send.

info(host, opts \\ [])

@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; see ADS.Codepage)

search(opts \\ [])

@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; see ADS.Codepage)