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

Master orchestrates startup, activation, deactivation, and runtime recovery
for the local EtherCAT session.

`EtherCAT.Master` is the public boundary for the master lifecycle. It owns
the singleton session exposed through `EtherCAT.state/0`, while internal
helpers own bus discovery, slave bring-up, activation, deactivation,
recovery, and status projection.

Before the master reports `:preop_ready` or starts OP activation, it
quiesces the bus. That extra drain window keeps late startup traffic from
leaking into the first public mailbox/configuration exchange or the first OP
transition datagrams.

## Lifecycle states

- `:idle` - no active session
- `:discovering` - scanning the bus, counting slaves, assigning stations, and preparing startup
- `:awaiting_preop` - waiting for configured slaves to reach PREOP
- `:preop_ready` - all configured slaves reached PREOP and the session is ready for activation or dynamic configuration
- `:deactivated` - the session stays live below OP on purpose
- `:operational` - cyclic runtime is active; non-critical slave-local faults may still be tracked
- `:activation_blocked` - the desired runtime target was not fully reached
- `:recovering` - critical runtime faults are being healed

## Startup sequencing

```mermaid
sequenceDiagram
    autonumber
    participant App
    participant Master
    participant Bus
    participant DC
    participant Domain
    participant Slave

    App->>Master: start/1
    Master->>Bus: count slaves, assign stations, verify link
    opt DC is configured
        Master->>DC: initialize clocks
    end
    Master->>Domain: start domains in open state
    Master->>Slave: start slave processes
    Slave->>Bus: reach PREOP through INIT, SII, and mailbox setup
    Slave->>Domain: register PDO layout
    Slave-->>Master: report ready at PREOP
    opt activation is requested and possible
        opt DC runtime is available
            Master->>DC: start runtime maintenance
        end
        Master->>Domain: start cyclic exchange
        opt DC lock is required
            Master->>DC: wait for lock
        end
        Master->>Slave: request SAFEOP
        Master->>Slave: request OP
    end
    Master-->>App: state becomes preop_ready, activation_blocked, or operational
```

## Recovery model

Domains, slaves, and DC report runtime faults back to the master. Critical
faults move the session into `:recovering`; slave-local non-critical faults
can remain visible while the master stays `:operational`.

```mermaid
stateDiagram-v2
    [*] --> idle
    idle --> discovering: start/1
    discovering --> awaiting_preop: configured slaves are still pending
    discovering --> idle: startup fails or stop/0
    awaiting_preop --> preop_ready: all slaves reached PREOP, no activation requested
    awaiting_preop --> operational: all slaves reached PREOP and activation succeeds
    awaiting_preop --> activation_blocked: activation is incomplete
    awaiting_preop --> idle: timeout, fatal startup failure, or stop/0
    preop_ready --> operational: activate/0 succeeds
    preop_ready --> activation_blocked: activate/0 is incomplete
    preop_ready --> recovering: critical runtime fault
    preop_ready --> idle: stop/0 or fatal subsystem exit
    deactivated --> operational: activate/0 succeeds
    deactivated --> preop_ready: deactivate to PREOP
    deactivated --> activation_blocked: target transition remains incomplete
    deactivated --> recovering: critical runtime fault
    deactivated --> idle: stop/0 or fatal subsystem exit
    operational --> recovering: critical runtime fault
    operational --> deactivated: deactivate/0 settles in SAFEOP
    operational --> preop_ready: deactivate to PREOP
    operational --> idle: stop/0 or fatal subsystem exit
    activation_blocked --> operational: activation failures clear and target is OP
    activation_blocked --> deactivated: transition failures clear and target is SAFEOP
    activation_blocked --> preop_ready: transition failures clear and target is PREOP
    activation_blocked --> recovering: runtime faults remain after activation retry
    activation_blocked --> idle: stop/0 or fatal subsystem exit
    recovering --> operational: critical runtime faults are cleared and target is OP
    recovering --> deactivated: critical runtime faults are cleared and target is SAFEOP
    recovering --> preop_ready: critical runtime faults are cleared and target is PREOP
    recovering --> idle: stop/0 or recovery fails
```

# `server`

```elixir
@type server() :: :gen_statem.server_ref()
```

# `t`

```elixir
@type t() :: %EtherCAT.Master{
  activatable_slaves: [atom()],
  activation_failures: %{optional(atom()) =&gt; term()},
  await_callers: [term()],
  await_operational_callers: [term()],
  base_station: non_neg_integer(),
  bus_ref: reference() | nil,
  dc_config: EtherCAT.DC.Config.t() | nil,
  dc_ref: reference() | nil,
  dc_ref_station: non_neg_integer() | nil,
  dc_stations: [non_neg_integer()],
  desired_runtime_target: :preop | :safeop | :op,
  domain_configs: [EtherCAT.Domain.Config.t()] | nil,
  domain_refs: %{optional(reference()) =&gt; atom()},
  frame_timeout_floor_ms: pos_integer(),
  frame_timeout_override_ms: pos_integer() | nil,
  last_failure: map() | nil,
  pending_preop: MapSet.t(atom()),
  runtime_faults: %{optional(term()) =&gt; term()},
  scan_poll_ms: pos_integer() | nil,
  scan_stable_ms: pos_integer() | nil,
  scan_window: [{integer(), non_neg_integer()}],
  slave_configs: [EtherCAT.Slave.Config.t()] | nil,
  slave_count: non_neg_integer() | nil,
  slave_faults: %{optional(atom()) =&gt; term()},
  slave_refs: %{optional(reference()) =&gt; atom()},
  slaves: [map()]
}
```

# `activate`

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

Drive the current session toward its operational target.

# `await_dc_locked`

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

Wait until the active DC runtime reports a locked status.

Returns a local master-call error if no DC runtime is currently available.

# `await_operational`

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

Wait until the master reaches full operational cyclic runtime.

# `await_running`

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

Wait until the master reaches a usable running state.

# `bus`

```elixir
@spec bus() ::
  EtherCAT.Bus.server()
  | nil
  | {:error, :not_started | :timeout | {:server_exit, term()}}
```

Return the stable local bus server reference, or `nil` if the session exists
but the bus subsystem is not currently running.

# `configure_slave`

```elixir
@spec configure_slave(atom(), keyword() | EtherCAT.Slave.Config.t()) ::
  :ok | {:error, term()}
```

Apply or replace runtime configuration for one named slave.

This is primarily used for dynamic PREOP-first workflows.

# `dc_status`

```elixir
@spec dc_status() ::
  EtherCAT.DC.Status.t()
  | {:error, :not_started | :timeout | {:server_exit, term()}}
```

Return the current Distributed Clocks runtime status snapshot.

# `deactivate`

```elixir
@spec deactivate(:safeop | :preop) :: :ok | {:error, term()}
```

Retreat the current session to `:safeop` or `:preop` while keeping the master
and bus runtime alive.

# `domains`

```elixir
@spec domains() :: list() | {:error, :not_started | :timeout | {:server_exit, term()}}
```

Return compact runtime snapshots for configured domains.

Each entry contains `{domain_id, live_cycle_time_us, pid}`.

# `last_failure`

```elixir
@spec last_failure() ::
  map() | nil | {:error, :not_started | :timeout | {:server_exit, term()}}
```

Return the last retained terminal failure snapshot, if any.

# `reference_clock`

```elixir
@spec reference_clock() ::
  {:ok, %{name: atom() | nil, station: non_neg_integer()}} | {:error, term()}
```

Return the currently selected DC reference clock as `%{name, station}`.

# `slaves`

```elixir
@spec slaves() :: list() | {:error, :not_started | :timeout | {:server_exit, term()}}
```

Return compact runtime snapshots for all tracked slaves.

Each entry includes the configured name, station, server reference, live pid,
and any currently tracked slave-local fault.

# `start`

```elixir
@spec start(keyword()) :: :ok | {:error, term()}
```

Start the singleton master session with the given startup options.

This is the direct module-level entry point behind `EtherCAT.start/1`.

# `state`

```elixir
@spec state() :: atom() | {:error, :not_started | :timeout | {:server_exit, term()}}
```

Return the current public master lifecycle state.

# `stop`

```elixir
@spec stop() :: :ok | :already_stopped | {:error, :timeout | {:server_exit, term()}}
```

Stop the current master session and tear down its runtime.

Returns `:already_stopped` when no local master process is running.

# `update_domain_cycle_time`

```elixir
@spec update_domain_cycle_time(atom(), pos_integer()) :: :ok | {:error, term()}
```

Update the live cycle time for one configured domain.

This does not mutate the stored startup config; it updates the running domain
process only.

---

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