# `ADS.Client`
[🔗](https://github.com/tallakt/yaads/blob/v0.1.0/lib/ads/client.ex#L1)

A connection to an ADS device, usually a TwinCAT PLC, over TCP or Secure ADS.

    {:ok, plc} = ADS.Client.start_link(host: "10.0.0.5", net_id: "5.36.69.176.1.1")

    {:ok, 1500} = ADS.Client.read(plc, "MAIN.nSpeed")
    :ok = ADS.Client.write(plc, "MAIN.nSpeed", 1600)
    {:ok, %{"nSpeed" => 1600, "fTemp" => 21.5}} = ADS.Client.read(plc, "MAIN.stPump")
    {:ok, [{:ok, 1600}, {:ok, true}]} = ADS.Client.read_many(plc, ["MAIN.nSpeed", "MAIN.bOn"])

    {:error, :device_symbol_not_found} = ADS.Client.read(plc, "MAIN.nNone")

The client talks to one system, by its AMS NetId, and to the PLC runtime on AMS port 851 unless
told otherwise: `ams_port:` for the client, or `port:` for one request. Every request gets its
result within its timeout, as a value: the client never exits the caller, and it never logs.
The errors are those listed in `ADS`.

## Routes

A TwinCAT system talks only to clients it has a route for: the client's NetId and its address,
added in TwinCAT's engineering, its system tray menu or a target's web interface. The client's
NetId is `:local_net_id`, by default its IP address on the connection with `.1.1` added. To a
client without a route, the system closes the connection, and the client tries again. So it does
to a second connection from a NetId it's already connected to: clients on one machine to the
same system share one client, or each has a NetId and a route of its own.

## Variables by name

`read/3` and `write/4` reach a variable by its name, as the PLC knows it, `"MAIN.nSpeed"` or
`"GVL.astPumps[2].fTemp"`. The client asks the PLC for a handle of the name, and for the
descriptions of its type (see `ADS.Symbol` and `ADS.DataType`), and keeps both for as long as
the connection lasts. Values are Elixir terms as in `ADS.Type`: a structure is a map, an array a
list. A handle that has gone stale, as after an online change, is looked up again, once.

`type:` gives the type instead, which saves asking for its description, and also reaches data by
index group and offset:

    {:ok, 1500} = ADS.Client.read(plc, "MAIN.nSpeed", type: :int)
    {:ok, 42} = ADS.Client.read(plc, {0x4020, 0}, type: :dint)
    :ok = ADS.Client.write(plc, {0x4020, 4}, [1, 2, 3], type: {:array, :int, [{1, 3}]})

`read_many/3` and `write_many/3` read and write many at once, in sum commands of up to 500, or
one at a time with a device that has none. `symbols/2` and `data_types/2` upload all of the
PLC's descriptions, and `request/3` sends any ADS command (see `ADS.Command`).

## Notifications

`subscribe/3` asks the PLC to send a variable's value when it changes, or every cycle, to the
subscriber, the caller by default:

    {:ok, ref} = ADS.Client.subscribe(plc, ["MAIN.nSpeed", "MAIN.bOn"], cycle_time: 100)

    # the current values at once, then each change
    receive do
      {ADS.Client, ^ref, {:value, "MAIN.nSpeed", speed, %DateTime{}}} -> speed
    end

| Message | When |
|---|---|
| `{ADS.Client, ref, {:value, target, value, timestamp}}` | a value, with the PLC's time of it in UTC |
| `{ADS.Client, ref, {:disconnected, reason}}` | the connection dropped; the values come again once it's back |
| `{ADS.Client, ref, {:error, target, reason}}` | a notification couldn't be added again, after a reconnect or an online change; the target is left out |

A subscription ends with `unsubscribe/2`, or when the subscriber exits. Beckhoff recommends at
most 550 notifications for a device.

## Many requests at once

Up to `:max_pending` requests are on the way at once, matched to their answers by invoke id;
more wait in the client, within their own timeouts. Callers may share a client, and
`send_request/3` lets one process have many requests out.

## Connections

`start_link/1` returns at once, and the client connects in the background: a request made while
it's connecting waits for the connection. When the connection drops, the client connects again
by itself, after 100 ms at first and up to 5 seconds between attempts (`:backoff`), and adds its
subscriptions' notifications again. While it's down, requests fail at once with
`{:error, :closed}`, and `status/1` says why. Two requests in a row that time out with nothing at
all heard since the first was sent mean the connection is dead, and the client reconnects.

## Secure ADS

`secure:` connects to port 8016 with TLS 1.2, as `ADS.Secure` describes, in one of three ways:

    # a pre-shared key, as in the PLC's StaticRoutes.xml
    secure: [psk: {"scada", "secret"}]

    # certificates from a CA both sides trust
    secure: [cacertfile: "ca.pem", certfile: "scada.crt", keyfile: "scada.key"]

    # self-signed: the PLC's certificate known by its fingerprint, and a route added once
    secure: [fingerprint: "6E:3B:...", certfile: "scada.pem", keyfile: "scada.key",
             add_route: {"Administrator", "1"}]

The PLC's NetId comes in its answer, so `net_id:` may be left out.

| Option | |
|---|---|
| `:psk` | `{identity, password}` |
| `:cacerts`, `:cacertfile` | the CA, for certificates from one |
| `:fingerprint` | the SHA-256 of the PLC's self-signed certificate in hex, as TwinCAT shows it, or `:any` |
| `:cert` and `:key`, or `:certfile` and `:keyfile` | the client's own certificate, for both kinds |
| `:add_route` | `{user, password}` of a user on the PLC, to add a route for a self-signed certificate |
| `:ignore_cn` | `true` for certificates that don't name their host: the PLC's isn't checked, and the PLC is asked not to check the client's |
| `:ip_address` | `true` for a route that names the client by address rather than host name |
| `:hostname` | the name the client gives, by default its host name |
| `:ssl` | more options for OTP's `:ssl` |

## Options

  * `:host` - the PLC's host name or address (required)
  * `:net_id` - the PLC's AMS NetId, such as `"5.36.69.176.1.1"` (required, except with
    `secure:`)
  * `:ams_port` - the AMS port of the device (default 851, the first PLC runtime)
  * `:local_net_id` - the client's NetId, as the PLC's route has it (default the client's IP
    address on the connection, with `.1.1`)
  * `:local_ams_port` - the client's AMS port (default one picked from 32768 to 65535, the range
    for clients)
  * `:port` - the TCP port (default 48898, or 8016 with `secure:`)
  * `:secure` - Secure ADS, as above
  * `:timeout` - how long a request may take, in ms, waiting its turn included (default 5000)
  * `:max_pending` - how many requests may be on the way at once (default 32)
  * `:connect_timeout` - in ms (default 5000)
  * `:backoff` - `{first, longest}` wait between attempts to connect, in ms
    (default `{100, 5000}`)
  * `:max_size` - the most data an answer may have, in bytes (default 16 MB)
  * `:codepage` - of the system's strings of one byte to a character: its STRINGs, the names
    and comments of its symbols and types, its host name, routes and adapters, and Secure ADS's
    host name and login (default `:windows_1252`; see `ADS.Codepage`)
  * `:name` - to register the process

# `item`

```elixir
@type item() :: target() | {target(), ADS.Type.t()}
```

A target, and the type of its value if it's to be given rather than looked up.

# `target`

```elixir
@type target() :: String.t() | {ADS.index_group(), ADS.index_offset()}
```

A variable, by its name, or by index group and offset (which needs a type).

# `adapters`

```elixir
@spec adapters(GenServer.server(), keyword()) ::
  {:ok, [ADS.Adapter.t()]} | {:error, ADS.error()}
```

The network adapters of the system, with their addresses, as its system service lists them (see
`ADS.Adapter`). `port:` asks another AMS port than 10000.

    {:ok, [%ADS.Adapter{addresses: [{{10, 0, 0, 5}, {255, 255, 255, 0}}]} | _]} =
      ADS.Client.adapters(plc)

# `data_type`

```elixir
@spec data_type(GenServer.server(), String.t(), keyword()) ::
  {:ok, ADS.DataType.t()} | {:error, ADS.error()}
```

The PLC's description of a data type, by name; see `ADS.DataType`.

# `data_types`

```elixir
@spec data_types(GenServer.server(), keyword()) ::
  {:ok, [ADS.DataType.t()]} | {:error, ADS.error()}
```

Uploads the descriptions of all the PLC's data types.

# `host_address`

```elixir
@spec host_address(GenServer.server(), String.t(), keyword()) ::
  {:ok, :inet.ip4_address()} | {:error, ADS.error()}
```

The IPv4 address the system finds for a host name, as its system service looks it up: on the
system, so with its DNS and its hosts file. `port:` asks another AMS port than 10000.

    {:ok, {10, 0, 0, 7}} = ADS.Client.host_address(plc, "scada-1")
    {:error, :device_not_found} = ADS.Client.host_address(plc, "nowhere.invalid")

# `host_name`

```elixir
@spec host_name(GenServer.server(), keyword()) ::
  {:ok, String.t()} | {:error, ADS.error()}
```

The host name of the system, as its system service answers it. `port:` asks another AMS port
than 10000.

    {:ok, "CX-1234AB"} = ADS.Client.host_name(plc)

# `info`

```elixir
@spec info(GenServer.server()) :: %{
  status: :connected | :connecting | {:disconnected, term()},
  local: {ADS.net_id(), ADS.ams_port()} | nil,
  remote: {ADS.net_id(), ADS.ams_port()} | nil
}
```

The status, and the AMS addresses of both ends as `{net_id, port}`: the client's own, and the
device's, once known.

# `read`

```elixir
@spec read(GenServer.server(), target(), keyword()) ::
  {:ok, term()} | {:error, ADS.error()}
```

Reads a variable, by its name or by `{index_group, index_offset}` with `type:`, and gives its
value as `ADS.Type` has it.

# `read_device_info`

```elixir
@spec read_device_info(GenServer.server(), keyword()) ::
  {:ok, %{name: String.t(), version: {byte(), byte(), 0..65535}}}
  | {:error, ADS.error()}
```

Reads the name and version of the device, such as `%{name: "Plc30 App", version: {3, 1, 4026}}`.

# `read_many`

```elixir
@spec read_many(GenServer.server(), [item()], keyword()) ::
  {:ok, ok: term(), error: ADS.error()}
```

Reads many variables at once, each a name, or `{target, type}`, and gives a result for each, in
order: `{:ok, value}` or `{:error, reason}`.

    {:ok, [{:ok, 1500}, {:error, :device_symbol_not_found}, {:ok, 42}]} =
      ADS.Client.read_many(plc, ["MAIN.nSpeed", "MAIN.nNone", {{0x4020, 0}, :dint}])

# `read_state`

```elixir
@spec read_state(GenServer.server(), keyword()) ::
  {:ok, %{ads_state: ADS.state(), device_state: 0..65535}}
  | {:error, ADS.error()}
```

Reads the ADS state of the device, `:run` for a running PLC, and its device state.

# `request`

```elixir
@spec request(GenServer.server(), ADS.Command.request(), keyword()) ::
  ADS.Command.result()
```

Sends a request and waits for its result; see `ADS.Command` for both. `port:` sends it to
another AMS port than the client's, and `timeout:` overrides the client's for it.

    ADS.Client.request(plc, {:read, 0x4020, 0, 4})
    ADS.Client.request(plc, :read_state, port: 10000)

# `routes`

```elixir
@spec routes(GenServer.server(), keyword()) ::
  {:ok, [ADS.Route.t()]} | {:error, ADS.error()}
```

The routes of the system, as its system service lists them: the clients and systems it talks
to, by name, NetId and address (see `ADS.Route`). `port:` asks another AMS port than 10000.

    {:ok, [%ADS.Route{name: "scada", net_id: "10.0.0.7.1.1", address: "10.0.0.7"} | _]} =
      ADS.Client.routes(plc)

# `send_request`

```elixir
@spec send_request(GenServer.server(), ADS.Command.request(), keyword()) ::
  reference()
```

Sends a request without waiting for it, and returns a reference. The result comes as a message
to the caller, or to the process `to:` in `opts`:

    {ADS.Client, ref, result}

It comes within the timeout, unless the client itself stops; monitor the client to hear of that.

# `start_link`

```elixir
@spec start_link(keyword()) :: GenServer.on_start()
```

Starts a client, linked to the caller, with the options above. It returns at once, and connects
in the background. Raises `ArgumentError` for options that don't make sense.

# `status`

```elixir
@spec status(GenServer.server()) :: :connected | :connecting | {:disconnected, term()}
```

Whether the client is connected: `:connected`, `:connecting`, or `{:disconnected, reason}` with
the reason it last couldn't connect or lost its connection, such as `:econnrefused` or
`:closed`.

# `stop`

```elixir
@spec stop(GenServer.server()) :: :ok
```

Closes the connection and stops the client, letting go of its handles and notifications: it
waits for the device to close its end too, so that it has read them, for a second at most.

# `subscribe`

```elixir
@spec subscribe(GenServer.server(), item() | [item()], keyword()) ::
  {:ok, reference()} | {:error, ADS.error()}
```

Asks the PLC to send the values of variables, each a name or `{target, type}`, to the
subscriber; see "Notifications" above.

  * `:mode` - `:on_change`, the value when it changes, checked every cycle time (the default),
    or `:cyclic`, every cycle time
  * `:cycle_time` - in ms (default 100)
  * `:max_delay` - the longest the PLC may hold a value back, to send it along with others, in ms
    (default 0)
  * `:to` - the subscriber (default the caller)

# `symbol`

```elixir
@spec symbol(GenServer.server(), String.t(), keyword()) ::
  {:ok, ADS.Symbol.t()} | {:error, ADS.error()}
```

The PLC's description of a variable, by name; see `ADS.Symbol`.

# `symbols`

```elixir
@spec symbols(GenServer.server(), keyword()) ::
  {:ok, [ADS.Symbol.t()]} | {:error, ADS.error()}
```

Uploads the descriptions of all the PLC's variables.

# `type`

```elixir
@spec type(GenServer.server(), String.t(), keyword()) ::
  {:ok, ADS.Type.t()} | {:error, ADS.error()}
```

The `ADS.Type` of a type name: built in, or as the PLC describes it, and those its description
refers to. The client keeps the types it has looked up for as long as the connection lasts.

    {:ok, {:struct, 16, [{"nSpeed", 0, :int}, {"fTemp", 8, :lreal}]}} = ADS.Client.type(plc, "ST_Pump")

# `unsubscribe`

```elixir
@spec unsubscribe(GenServer.server(), reference()) :: :ok
```

Ends a subscription, and asks the PLC to delete its notifications.

# `write`

```elixir
@spec write(GenServer.server(), target(), term(), keyword()) ::
  :ok | {:error, ADS.error()}
```

Writes a variable, by its name or by `{index_group, index_offset}` with `type:`. Raises
`ArgumentError` for a value that doesn't fit its type, before writing anything.

# `write_control`

```elixir
@spec write_control(GenServer.server(), ADS.state(), keyword()) ::
  :ok | {:error, ADS.error()}
```

Changes the ADS state of the device: `:run` starts a PLC, `:stop` stops it, and `:reset` resets
it. `device_state:` and `data:` go along, 0 and nothing by default.

# `write_many`

```elixir
@spec write_many(
  GenServer.server(),
  [{target(), term()} | {target(), term(), ADS.Type.t()}],
  keyword()
) ::
  {:ok, [:ok | {:error, ADS.error()}]}
```

Writes many variables at once, each `{target, value}` or `{target, value, type}`, and gives a
result for each, in order: `:ok` or `{:error, reason}`. Raises `ArgumentError` for a value that
doesn't fit its type, before writing anything.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
