# `EtherCAT.Bus.Transport`
[🔗](https://github.com/sid2baker/ethercat/blob/main/lib/ethercat/bus/transport.ex#L1)

Behaviour for EtherCAT frame transports.

Modelled after `:gen_udp`'s `{active, once}` pattern:
arm once, receive one message containing the EtherCAT payload, repeat.

## Implementations

  - `EtherCAT.Bus.Transport.RawSocket` — AF_PACKET raw Ethernet
  - `EtherCAT.Bus.Transport.UdpSocket` — UDP/IP encapsulation (spec §2.6)

## Async receive flow

    # After sending a frame, arm for one delivery:
    transport_mod.set_active_once(transport)

    # In gen_statem handle_event(:info, msg, :awaiting, data):
    case transport_mod.match(transport, msg) do
      {:ok, ecat_payload, rx_at, _src_mac} -> # process response
      {:error, reason} -> # transport-level receive failure
      :ignore -> :keep_state_and_data
    end

# `t`

```elixir
@type t() :: struct()
```

# `close`

```elixir
@callback close(t()) :: t()
```

Close the transport. Returns the struct with the socket set to nil.

# `drain`

```elixir
@callback drain(t()) :: :ok
```

Drain buffered frames and cancel any pending select/active registration.

# `interface`

```elixir
@callback interface(t()) :: String.t() | nil
```

Returns the network interface name, or `nil` if not applicable.

Used by the internal bus link monitor. Returns `nil` for transports that
don't rely on a named OS interface (e.g. `UdpSocket`).

# `match`

```elixir
@callback match(t(), msg :: term()) ::
  {:ok, ecat_payload :: binary(), rx_at :: integer(),
   frame_src_mac :: binary() | nil}
  | {:error, reason :: term()}
  | :ignore
```

Decode one process mailbox message from this transport.

Returns `{:ok, ecat_payload, rx_at, frame_src_mac}` when the message belongs
to this transport and data is ready; `{:error, reason}` when the message
belongs to this transport but receive processing failed; `:ignore` for all
other messages.

`frame_src_mac` is the 6-byte source MAC address from the Ethernet header
of the received frame, or `nil` for transports that don't expose it (UDP).
The redundant link uses this to distinguish cross-over frames (sent by the
other port's NIC) from own-port loopback frames.

- `RawSocket`: matches `{:"$socket", raw, :select, _}`, calls `recvmsg`
  internally, strips Ethernet headers. The two-step select dance is hidden.
- `UdpSocket`: matches `{:udp, raw, _ip, _port, data}` and returns data inline.

# `name`

```elixir
@callback name(t()) :: String.t()
```

Returns a human-readable transport identifier for telemetry and logging.

- `RawSocket`: returns the interface name (e.g. `"eth0"`)
- `UdpSocket`: returns `"host:port"` (e.g. `"192.168.1.1:34980"`)

# `open`

```elixir
@callback open(keyword()) :: {:ok, t()} | {:error, term()}
```

Open a transport from keyword options.

# `open?`

```elixir
@callback open?(t()) :: boolean()
```

Returns `true` when the transport is open.

# `rearm`

```elixir
@callback rearm(t()) :: :ok
```

Re-arm for the next async delivery without draining buffered frames.

Unlike `set_active_once/1`, this preserves any frames already in the
socket buffer. Used by the bus after an idx-mismatch so that a
legitimate response sitting behind a rogue frame is not lost.

- `RawSocket`: self-sends a synthetic select notification so `match/2`
  re-enters and reads the next buffered frame.
- `UdpSocket`: delegates to `set_active_once/1` (no drain issue).

# `send`

```elixir
@callback send(t(), ecat_payload :: binary()) ::
  {:ok, tx_at :: integer()} | {:error, term()}
```

Send an EtherCAT payload. Returns monotonic tx timestamp on success.

The transport is responsible for any outer framing (Ethernet headers,
UDP/IP headers). The payload is the output of `Bus.Frame.encode/1`.

# `set_active_once`

```elixir
@callback set_active_once(t()) :: :ok
```

Arm for exactly one async delivery — analogous to `inet:setopts([{active, once}])`.

After calling this, the next received frame will be delivered as a message
to the calling process. Use `match/2` to decode it.

- `RawSocket`: calls `:socket.recvmsg(:nowait)` to register the kernel select
- `UdpSocket`: calls `:inet.setopts([{:active, :once}])`

# `src_mac`

```elixir
@callback src_mac(t()) :: &lt;&lt;_::48&gt;&gt; | nil
```

Returns the transport's own source MAC address (6 bytes), or `nil`.

Used by the redundant link to identify whether a received frame
originated from this port's NIC or crossed over from the other port.
Returns `nil` for transports without MAC addresses (e.g. UDP).

---

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