# `EtherCAT.Domain`
[🔗](https://github.com/sid2baker/ethercat/blob/main/lib/ethercat/domain.ex#L1)

Cyclic process image for one logical EtherCAT domain.

One domain runs per configured domain ID. Slaves register their PDO layout
during PREOP, then the domain runs a self-timed LRW exchange each cycle.

`EtherCAT.Domain` is the public boundary for domain lifecycle and process
image access. The domain process owns the open, cycling, and stopped states.
The hot-path image API is intentionally separate: `write/3`, `read/2`, and
`sample/2` access the ETS-backed process image directly instead of going
through synchronous state-machine calls.

## States

- `:open` - accepting PDO registrations, not yet cycling
- `:cycling` - self-timed LRW tick active
- `:stopped` - cycling halted by manual stop or miss threshold

## State transitions

```mermaid
stateDiagram-v2
    [*] --> open
    open --> cycling: start_cycling and layout preparation succeed
    cycling --> stopped: stop_cycling or miss threshold is reached
    stopped --> cycling: start_cycling
```

Within `:cycling`, cycle health is tracked separately as runtime data:

- `:healthy` - the latest LRW cycle was valid
- `{:invalid, reason}` - the latest LRW cycle had a transport miss or unusable reply

Timeout-class misses remain `:timeout` in domain health even when the bus
dropped a stale realtime cycle request as `:expired`; the finer queue-expiry
distinction stays in bus telemetry.

# `domain_id`

```elixir
@type domain_id() :: atom()
```

# `freshness_info`

```elixir
@type freshness_info() :: %{
  state: :not_ready | :fresh | :stale,
  refreshed_at_us: integer() | nil,
  age_us: non_neg_integer() | nil,
  stale_after_us: pos_integer()
}
```

# `pdo_key`

```elixir
@type pdo_key() :: {slave_name :: atom(), pdo_name :: atom()}
```

# `sample_info`

```elixir
@type sample_info() :: %{
  value: binary(),
  updated_at_us: integer() | nil,
  changed_at_us: integer() | nil,
  freshness: freshness_info() | nil
}
```

# `info`

```elixir
@spec info(domain_id()) ::
  {:ok, map()} | {:error, :not_found | :timeout | {:server_exit, term()}}
```

Return a detailed runtime snapshot for the domain.

# `read`

```elixir
@spec read(domain_id(), pdo_key()) ::
  {:ok, binary()} | {:error, :not_found | :not_ready}
```

Read the current process-image value for one PDO key.

Returns `{:error, :not_ready}` until an input has been populated or an output
has been staged.

# `register_pdo`

```elixir
@spec register_pdo(domain_id(), pdo_key(), pos_integer(), :input | :output) ::
  {:ok, non_neg_integer()} | {:error, term()}
```

Register one PDO entry in the domain layout while the domain is open.

Returns the assigned logical byte offset relative to the domain base.

# `sample`

```elixir
@spec sample(domain_id(), pdo_key()) ::
  {:ok, sample_info()} | {:error, :not_found | :not_ready}
```

Read the current process-image value plus freshness metadata for one PDO key.

# `start_cycling`

```elixir
@spec start_cycling(domain_id()) :: :ok | {:error, term()}
```

Start cyclic LRW exchange for the domain.

# `stats`

```elixir
@spec stats(domain_id()) ::
  {:ok, map()} | {:error, :not_found | :timeout | {:server_exit, term()}}
```

Return a compact runtime statistics snapshot for the domain.

# `stop_cycling`

```elixir
@spec stop_cycling(domain_id()) ::
  :ok | {:error, :not_found | :timeout | {:server_exit, term()}}
```

Stop cyclic LRW exchange for the domain.

The process image remains available after the domain stops.

# `update_cycle_time`

```elixir
@spec update_cycle_time(domain_id(), pos_integer()) :: :ok | {:error, term()}
```

Update the live cycle period for the running domain.

# `write`

```elixir
@spec write(domain_id(), pdo_key(), binary()) :: :ok | {:error, :not_found}
```

Stage an output binary into the domain process image.

This is a direct ETS-backed hot-path write and does not go through the domain
process mailbox.

---

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