# `EtherCAT.Slave.Driver`
[🔗](https://github.com/sid2baker/ethercat/blob/main/lib/ethercat/slave/driver.ex#L1)

Behaviour for slave-specific drivers.

A driver is a pure module — no process state, no `Application.get_env`.
All configuration is passed as a `config` map from `EtherCAT.start/1`.

If a slave config omits `:driver`, `EtherCAT.Slave.Driver.Default` is used.
That default driver exposes no PDO profile and is intended for couplers or
dynamically configured devices.

Generic SYNC0/SYNC1/latch intent does not live in the driver. It belongs on
`%EtherCAT.Slave.Config{sync: %EtherCAT.Slave.Sync.Config{...}}`. Drivers only own
device-specific translation through the optional `sync_mode/2` callback when a
slave application needs additional mailbox objects beyond the generic ESC DC
registers.

## Optional identity and simulator support

Drivers may optionally declare static device identity through `identity/0`.
That metadata is useful for future driver discovery against scanned bus
identity and for tooling. Keep it high-level: vendor, product, and revision.

Exact simulator authoring does not live in the real driver behaviour. Use an
optional `MyDriver.Simulator` companion module that implements
`EtherCAT.Simulator.DriverAdapter` when
`EtherCAT.Simulator.Slave.from_driver/2` needs profile-specific simulator
configuration.

## Signal model

`signal_model/1` returns a keyword list of `{signal_name, declaration}` pairs.
Each value declares where that signal lives in the slave's PDO layout:

- an integer means "this signal spans the whole PDO at that index"
- `%EtherCAT.Slave.ProcessData.Signal{}` may select a bit-range inside a PDO

The master reads SII EEPROM categories 0x0032 (TxPDO) and 0x0033 (RxPDO) to
derive SyncManager assignment, direction, total SM size, and each PDO's bit
offset within its SyncManager. The driver's signal model sits on top of that
hardware description and names the application-facing signals.

Use a keyword list (not a map) so signal order is explicit and deterministic.

    [channels: 0x1A00]

Each signal is encoded and decoded independently. Sub-byte signals (e.g. 1-bit
digital channels) receive/return 1 padded byte with the value in bit 0 (LSB).
Larger signals receive exactly enough bytes to carry the declared `bit_size`.

## Example — EL1809 / EL2809 digital cards

Full compiled example drivers live in the test integration support tree:

- `test/integration/support/drivers/el1809.ex`
- `test/integration/support/drivers/el2809.ex`

They show the minimal `signal_model/1` and bit-oriented
`encode_signal/3` / `decode_signal/3` callbacks for Beckhoff-style
16-channel digital I/O terminals.

## Example — EL3202 (2-ch PT100 input, 2 × 32-bit TxPDOs)

    defmodule MyApp.EL3202 do
      @behaviour EtherCAT.Slave.Driver

      @impl true
      def signal_model(_config) do
        # 0x1A00 = channel 1 (SM3, bytes 0–3), 0x1A01 = channel 2 (SM3, bytes 4–7)
        [channel1: 0x1A00, channel2: 0x1A01]
      end

      @impl true
      def mailbox_config(_config) do
        [
          {:sdo_download, 0x8000, 0x19, <<8::16-little>>},
          {:sdo_download, 0x8010, 0x19, <<8::16-little>>}
        ]
      end

      @impl true
      def encode_signal(_signal, _config, _value), do: <<>>

      @impl true
      def decode_signal(:channel1, _config, <<
            _::1, error::1, _::2, _::2, overrange::1, underrange::1,
            toggle::1, state::1, _::6, value::16-little>>) do
        %{ohms: value / 16.0, overrange: overrange == 1, underrange: underrange == 1,
          error: error == 1, invalid: state == 1, toggle: toggle}
      end
      def decode_signal(:channel2, _config, <<
            _::1, error::1, _::2, _::2, overrange::1, underrange::1,
            toggle::1, state::1, _::6, value::16-little>>) do
        %{ohms: value / 16.0, overrange: overrange == 1, underrange: underrange == 1,
          error: error == 1, invalid: state == 1, toggle: toggle}
      end
      def decode_signal(_signal, _config, _), do: nil
    end

# `config`

```elixir
@type config() :: map()
```

# `identity`

```elixir
@type identity() :: %{
  :vendor_id =&gt; non_neg_integer(),
  :product_code =&gt; non_neg_integer(),
  optional(:revision) =&gt; non_neg_integer() | :any
}
```

# `latch_edge`

```elixir
@type latch_edge() :: :pos | :neg
```

# `mailbox_step`

```elixir
@type mailbox_step() ::
  {:sdo_download, index :: non_neg_integer(), subindex :: non_neg_integer(),
   data :: binary()}
```

# `signal_name`

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

# `decode_signal`

```elixir
@callback decode_signal(signal_name(), config(), binary()) :: term()
```

Decode raw input bytes for one logical signal from the process image.

# `encode_signal`

```elixir
@callback encode_signal(signal_name(), config(), term()) :: binary()
```

Encode one logical output signal into raw bytes for the process image.

# `identity`

```elixir
@callback identity() :: identity() | nil
```

Static device identity for this driver, or `nil` if the driver does not
declare one.

This is useful for future driver discovery against scanned bus identity and
for simulator hydration. Keep it high-level: vendor/product/revision only.
Do not put raw ESC register images or SII binaries into the driver API.

# `mailbox_config`
*optional* 

```elixir
@callback mailbox_config(config()) :: [mailbox_step()]
```

Return PREOP mailbox configuration steps.

Currently the runtime supports `{:sdo_download, index, subindex, data}` steps.
They execute in order before SyncManager/FMMU configuration, so this callback
can perform dynamic PDO remapping (`0x1600+`, `0x1A00+`, `0x1C12`, `0x1C13`)
or any other CoE parameterization required before SAFEOP.

`data` may be any non-empty binary. The runtime selects expedited or
segmented CoE transfer mode automatically.

# `on_latch`
*optional* 

```elixir
@callback on_latch(atom(), config(), 0 | 1, latch_edge(), non_neg_integer()) :: :ok
```

Called when an ESC hardware LATCH event is captured during Op.

`timestamp_ns` is DC system time in ns since 2000-01-01.

# `on_op`
*optional* 

```elixir
@callback on_op(slave_name :: atom(), config()) :: :ok
```

Called on entry to Op state. Optional.

# `on_preop`
*optional* 

```elixir
@callback on_preop(slave_name :: atom(), config()) :: :ok
```

Called on entry to PreOp state. Optional.

# `on_safeop`
*optional* 

```elixir
@callback on_safeop(slave_name :: atom(), config()) :: :ok
```

Called on entry to SafeOp state. Optional.

# `signal_model`

```elixir
@callback signal_model(config()) :: [
  {signal_name(), non_neg_integer() | EtherCAT.Slave.ProcessData.Signal.t()}
]
```

Return the driver's logical signal model.

Each signal maps to either a whole PDO index or a `%Signal{}` slice.

An optional 2-arity version `signal_model/2` receives the SII PDO configs
as its second argument. When exported, the runtime calls it instead of `/1`, giving
the driver access to hardware layout for dynamic model generation. The `Default`
driver uses this to auto-discover all PDOs without any hand-written mapping.

# `signal_model`
*optional* 

```elixir
@callback signal_model(config(), sii_pdo_configs :: [map()]) :: [
  {signal_name(), non_neg_integer() | EtherCAT.Slave.ProcessData.Signal.t()}
]
```

Optional 2-arity variant of `signal_model/1` that receives SII PDO configs.

Each entry in `sii_pdo_configs` is a map with keys:
  - `:index` — PDO object index (e.g. `0x1A00`)
  - `:direction` — `:input` or `:output`
  - `:sm_index` — SyncManager index
  - `:bit_size` — total PDO size in bits
  - `:bit_offset` — PDO offset within its SyncManager image in bits

When this callback is exported, it takes precedence over `signal_model/1`.

# `sync_mode`
*optional* 

```elixir
@callback sync_mode(config(), EtherCAT.Slave.Sync.Config.t()) :: [mailbox_step()]
```

Translate public sync intent into device-specific PREOP mailbox steps.

Use this for slaves that need object-dictionary sync-mode configuration
(for example `0x1C32` / `0x1C33`) in addition to the generic ESC SYNC setup
handled by the runtime.

`EtherCAT.Slave.Sync.CoE` provides helpers for the common synchronization mode and
cycle-time objects when a driver wants to avoid hand-writing raw SDO tuples.

# `identity`

```elixir
@spec identity(module()) :: identity() | nil
```

# `signal_model`

```elixir
@spec signal_model(module(), config()) :: [
  {signal_name(), non_neg_integer() | EtherCAT.Slave.ProcessData.Signal.t()}
]
```

# `signal_model`

```elixir
@spec signal_model(module(), config(), [map()]) :: [
  {signal_name(), non_neg_integer() | EtherCAT.Slave.ProcessData.Signal.t()}
]
```

---

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