EtherCAT

Hex version Hexdocs License

Pure-Elixir EtherCAT master for Linux and Nerves, built on OTP without NIFs or kernel modules. Intended for discrete I/O, diagnostics, and soft-real-time 1–10 ms cyclic loops—not sub-millisecond hard-real-time control.

Experimental; not production-ready. This project explores soft-real-time automation. I would like to develop it into a bachelor's thesis; if you know a professor who could help, please reach out.

Want an interactive UI? Start with kino_ethercat. Without hardware, try the UDP simulator walkthrough or run the simulator tests below.

1. Install and supervise the runtime

This guide targets EtherCAT 0.5.0. Add the Hex dependency to your application's mix.exs:

def deps do
  [{:ethercat, "~> 0.5.0"}]
end

Use the 0.5.0 API reference. Upgrading from 0.4.x? Read the migration notes below; 0.5.0 changes the runtime and driver APIs without compatibility shims.

Run mix deps.get, then add {EtherCAT.Runtime, []} to your application's supervision tree. EtherCAT does not start the runtime automatically. For a standalone iex -S mix session, start it once with:

{:ok, _supervisor} =
  Supervisor.start_link([{EtherCAT.Runtime, []}], strategy: :one_for_one)

2. Discover your hardware

Use a dedicated Ethernet interface connected to your EtherCAT ring. Raw socket access needs root or CAP_NET_RAW. To grant the capability to your current BEAM executable (this affects every application using that executable):

BEAM=$(readlink -f "$(dirname "$(dirname "$(command -v erl)")")"/erts-*/bin/beam.smp)
sudo setcap cap_net_raw+ep "$BEAM"

Restart IEx/the application after granting the capability. Replace eth0 with your interface. Scan before opening a master session; scanning assigns station addresses and must not run against an active ring.

backend = {:raw, %{interface: "eth0"}}
{:ok, scan} = EtherCAT.Scan.scan(backend)
scan.discovered_slaves

To inspect slaves without cyclic I/O, start in PREOP:

{:ok, session} = EtherCAT.start(backend: backend)
:ok = EtherCAT.await_ready(session)
EtherCAT.Diagnostics.slaves(session)
:ok = EtherCAT.stop(session)

3. Read a cyclic input

Drivers are application-owned. This minimal driver reads channel 1 of an EL1809; static endpoint metadata is optional. For shared mappings and metadata, see the driver authoring guide.

defmodule MyApp.EL1809 do
  @behaviour EtherCAT.Driver

  @impl true
  def signal_model(_config, _pdos), do: [ch1: EtherCAT.Driver.Signal.whole_pdo(0x1A00)]

  @impl true
  def decode_signal(_signal, _config, <<0::7, bit::1>>), do: {:ok, bit == 1}
  def decode_signal(_signal, _config, _raw), do: {:error, :invalid_data}
end

List slaves in physical ring order. This example assumes an EK1100 coupler followed by an EL1809; adapt it to the hardware you discovered before starting:

{:ok, session} = EtherCAT.start(
  backend: backend,
  domains: [%EtherCAT.Domain.Config{id: :io, cycle_time_us: 1_000}],
  slaves: [
    %EtherCAT.Slave.Config{name: :coupler},
    %EtherCAT.Slave.Config{
      name: :inputs,
      driver: MyApp.EL1809,
      process_data: {:all, :io},
      target_state: :op
    }
  ]
)

:ok = EtherCAT.await_operational(session)
EtherCAT.sample(session, :inputs, :io)
#=> {:ok, %EtherCAT.Sample{inputs: %{ch1: false}, errors: %{}, ...}}
# May return {:error, :not_ready} before the first sample arrives.

:ok = EtherCAT.stop(session)

Keep the returned session for every runtime call. Stopping it leaves the host-supervised runtime alive; reusing a stopped session returns {:error, :stale_session}. A sample covers one domain cycle, not a cross-domain snapshot. Check its errors as well as its inputs.

4. Diagnose problems

Inspect a live session before stopping it:

EtherCAT.state(session)
EtherCAT.Diagnostics.master_status(session)
EtherCAT.status(session, :inputs)
Symptom Action
Raw socket permission failure Check the interface and capability on the BEAM executable actually running your app.
Session stays in PREOP Use target_state: :op and attach signals to a domain; await_ready/1 does not require cyclic I/O.
Activation fails or state is :recovering Inspect master and slave status for transport, WKC, AL-state, or configuration faults.
Sample has decoding errors Check the driver's mapping, bit widths, and codec; failed values are not replaced with zero or earlier values.
Subscriber mailbox grows Poll with EtherCAT.samples/2 instead; subscriptions have no backpressure or dropping.

Upgrade guide

For applications using 0.4.x:

0.4.x call 0.5.0 replacement
EtherCAT.await_running() EtherCAT.await_ready(session); use await_operational(session) for cyclic I/O
EtherCAT.read_input(slave, signal) EtherCAT.read(session, slave, signal)
EtherCAT.write_output(slave, signal, value) EtherCAT.write(session, slave, signal, value)
EtherCAT.subscribe(slave, signal) EtherCAT.Signals.subscribe(session, slave, signal)
EtherCAT.configure_slave(slave, opts) / activate() / deactivate() EtherCAT.Provisioning equivalents, with the session first
SDO transfers and DC lock waits on EtherCAT EtherCAT.Provisioning, with the session first
Slave/domain/DC inspection on EtherCAT EtherCAT.Diagnostics, with the session first
EtherCAT.stop() EtherCAT.stop(session)

EtherCAT.slaves(session) returns configured names; use EtherCAT.Diagnostics.slaves(session) for detailed summaries. For coherent per-domain observations, use sample/3 or subscribe/3 on EtherCAT. Protocol subscriptions return {:ok, ref, status, samples} and deliver {:ethercat, ref, payload} messages; cancel them with unsubscribe/3.

Migrate custom drivers from EtherCAT.Slave.Driver to EtherCAT.Driver. Implement signal_model/2 with named EtherCAT.Driver.Signal mappings, and return {:ok, value} or {:error, reason} from codecs. Mailbox setup moves to c:EtherCAT.Driver.Provisioning.mailbox_steps/2; consume lifecycle notifications and latch subscriptions instead of inline driver hooks. Simulator companions implement EtherCAT.Simulator.Adapter and supply an explicit profile. See the driver contract and full changelog.

Test without hardware

From a repository checkout:

mix deps.get
ETHERCAT_INTEGRATION_TRANSPORT=udp mix test test/integration/simulator --exclude raw_socket --exclude raw_socket_redundant --exclude raw_socket_redundant_toggle
mix test test/ethercat/driver/catalogue_example_test.exs

The simulator suite exercises the real master over virtual slave segments. The driver comparison demonstrates a shared signal catalogue without changing the public callbacks. The command above explicitly excludes raw transports. Hardware and raw-socket tests need separate setup.

Pick your next task