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

The nine ADS commands: the data of their requests and of the responses to them, inside AMS
packets (see `ADS.AMS`). All numbers are little-endian.

A request is a tuple, and its result is a value:

| Request | Command | Result |
|---|---|---|
| `:read_device_info` | 1 | `{:ok, %{name: name, version: {major, minor, build}}}` |
| `{:read, index_group, index_offset, length}` | 2 | `{:ok, binary}` |
| `{:write, index_group, index_offset, binary}` | 3 | `:ok` |
| `:read_state` | 4 | `{:ok, %{ads_state: state, device_state: integer}}` |
| `{:write_control, ads_state, device_state, binary}` | 5 | `:ok` |
| `{:add_notification, index_group, index_offset, length, mode, max_delay, cycle_time}` | 6 | `{:ok, handle}` |
| `{:delete_notification, handle}` | 7 | `:ok` |
| `{:read_write, index_group, index_offset, read_length, binary}` | 9 | `{:ok, binary}` |

Command 8, the device notification, goes the other way: a device sends it, unasked and
unanswered, with the samples of the notifications a client added (`decode_notification/1`).

The mode of a notification is `:cyclic`, every cycle time, or `:on_change`, checked every cycle
time; Beckhoff's documentation calls them ADSTRANS_SERVERCYCLE (3) and ADSTRANS_SERVERONCHA (4).
Its max delay and cycle time are in units of 100 ns on the wire, as in the ADS-DLL's
AdsNotificationAttrib and its samples, where a cycle time of 10000000 is a second; the ADS spec's
table of the command says 1 ms, which the TwinCAT devices don't follow.

    iex> ADS.Command.encode_request({:read, 0x4020, 0, 4})
    {2, <<0x20, 0x40, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0>>}
    iex> ADS.Command.decode_response({:read, 0x4020, 0, 4}, <<0::32, 4::32-little, 42::32-little>>)
    {:ok, <<42, 0, 0, 0>>}
    iex> ADS.Command.decode_response({:read, 0xF005, 7, 4}, <<0x10, 0x07, 0, 0>>)
    {:error, :device_symbol_not_found}

# `index_group`

```elixir
@type index_group() :: ADS.index_group()
```

# `index_offset`

```elixir
@type index_offset() :: ADS.index_offset()
```

# `mode`

```elixir
@type mode() :: :cyclic | :on_change | 0..4_294_967_295
```

# `request`

```elixir
@type request() ::
  :read_device_info
  | {:read, index_group(), index_offset(), 0..4_294_967_295}
  | {:write, index_group(), index_offset(), binary()}
  | :read_state
  | {:write_control, ADS.state(), 0..65535, binary()}
  | {:add_notification, index_group(), index_offset(), 0..4_294_967_295, mode(),
     0..4_294_967_295, 0..4_294_967_295}
  | {:delete_notification, 0..4_294_967_295}
  | {:read_write, index_group(), index_offset(), 0..4_294_967_295, binary()}
```

# `result`

```elixir
@type result() :: :ok | {:ok, term()} | {:error, ADS.error()}
```

# `stamp`

```elixir
@type stamp() :: {DateTime.t() | nil, [{handle :: 0..4_294_967_295, binary()}]}
```

The samples of a device notification: a timestamp, and each notification's data.

# `command_id`

```elixir
@spec command_id(request() | :device_notification) :: 1..9
```

The command id of a request: 1 to 9, as in the AMS header.

# `decode_notification`

```elixir
@spec decode_notification(binary()) :: {:ok, [stamp()]} | :error
```

The stamps of a device notification, each with the samples taken at its time, or `:error` for
data that isn't one. A sample of no data says that its handle is no longer valid: the device
sends one such, once, when its variable goes away, as after an online change.

The timestamps are FILETIMEs, in UTC, here a `DateTime` with microseconds, the last digit of its
100 ns dropped; or nil for one past what `DateTime` can hold.

# `decode_response`

```elixir
@spec decode_response(request(), binary()) :: result()
```

The result of a request from the data of its response. A result other than 0 is the error of
that return code; a response that doesn't fit the request, such as a read that gives more than
asked for, is `{:error, {:invalid_response, data}}`.

# `encode_notification`

```elixir
@spec encode_notification([stamp()]) :: binary()
```

A device notification's data, from its stamps: the inverse of `decode_notification/1`, for
tests and for those who play a device.

# `encode_request`

```elixir
@spec encode_request(request()) :: {1..9, binary()}
```

The command id and data of a request. Raises `ArgumentError` for one that doesn't fit the
protocol, such as an index group past 32 bits.

# `from_filetime`

```elixir
@spec from_filetime(non_neg_integer()) :: DateTime.t() | nil
```

A FILETIME, 100 ns intervals since 1601-01-01 in UTC, as a `DateTime`, or nil for one it can't
hold.

    iex> ADS.Command.from_filetime(116_444_736_000_000_000)
    ~U[1970-01-01 00:00:00.000000Z]

# `to_filetime`

```elixir
@spec to_filetime(DateTime.t() | nil) :: non_neg_integer()
```

A `DateTime` as a FILETIME.

---

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