EtherCAT.Bus.Link.Redundant (ethercat v0.5.0)

Copy Markdown View Source

Dual-port bus link implemented as a gen_statem.

Owns two transports (primary and secondary), scheduling (queues, batching, stale expiry), in-flight exchange tracking, and caller replies.

In production this link is raw-only: both ports are expected to be EtherCAT.Bus.Transport.RawSocket interfaces. The transport_mod option remains available for tests and transport fakes.

Ring topology

In a healthy EtherCAT redundant ring, frames cross between ports:

  • Primary TX → slaves (forward) → Secondary RX
  • Secondary TX → slaves (reverse) → Primary RX

Each received frame is classified by comparing its Ethernet source MAC against the MAC addresses of both NICs.

Exchange flow

Every exchange attempts to send on both ports, starts a timeout if at least one send succeeds, and saves the caller. Frames are classified on arrival:

  • Cross-delivery — frame arrived on the opposite port (src MAC belongs to the other NIC). Ring path is healthy.
  • Bounce-back — frame arrived on the same port it was sent from (src MAC matches own NIC). Ring is broken; the frame reflected at the break point.

The forward cross (primary's frame on secondary) is authoritative and completes immediately. A single reverse cross or bounce keeps the merge window open while the link waits for a forward cross or second frame. A single :unknown arrival with processed datagrams (wkc > 0) can also complete immediately when MAC classification is not trustworthy. On timeout, partial arrivals are converted into a best-effort reply when they still match the awaiting datagrams; pure reverse-path copies are not used as authoritative data, so only processed or merged bounce data can rescue a timed-out exchange. Reliable non-logical one-sided bounces may also complete immediately when only a tiny remainder of the original frame-time budget is left, avoiding a spurious extra millisecond timer hop near the deadline.

flowchart TD
    IDLE -->|transact| SEND
    SEND[Attempt primary and secondary send] -->|any leg sent| WAIT
    SEND -->|both sends fail| ERR_SEND

    WAIT{Arrival or timeout}
    WAIT -->|forward cross| REPLY_OK
    WAIT -->|single authoritative reply on one live leg| REPLY_OK
    WAIT -->|single unknown with wkc>0| REPLY_OK
    WAIT -->|reverse cross / bounce / other unknown| MERGE_WAIT
    WAIT -->|timeout with no arrivals| ERR_TIMEOUT

    MERGE_WAIT{Merge window}
    MERGE_WAIT -->|forward cross arrives| REPLY_OK
    MERGE_WAIT -->|pri bounce + sec bounce| MERGE_OK
    MERGE_WAIT -->|timeout with partial arrivals| BEST_EFFORT

    REPLY_OK([Reply awaiting callers]) --> IDLE
    MERGE_OK([Merge complementary bounces]) --> IDLE
    BEST_EFFORT([Reply with best available datagrams]) --> IDLE
    ERR_TIMEOUT([Reply timeout]) --> IDLE
    ERR_SEND([Reply transport unavailable]) --> IDLE

gen_statem states

  • :idle — no exchange in flight, ready to dispatch
  • :awaiting — exchange sent, waiting for reply(ies)

The link tracks per-port transport health from open/send failures and surfaces degraded topology and fault info through info/1. Health is observational only: a leg marked :down is still attempted on the next exchange, so reconnects do not wait on a separate health promotion path. Timeout observations are logged and emitted as telemetry, but receive-side timeout patterns do not directly mark a port down unless send/open state also fails.

Summary

Functions

start_link(opts)

@spec start_link(keyword()) :: :gen_statem.start_ret()