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.
This guide targets EtherCAT 0.5.0. Add the Hex dependency to your
application's mix.exs:
def deps do
[{:ethercat, "~> 0.5.0"}]
endUse 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)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.
On macOS, for a rack on a desk, the same {:raw, %{interface: "en7"}} backend
works through a BPF device and a small C port program, which is compiled with
the system's C compiler the first time it is used. It needs read and write
access to /dev/bpf*: root, or the access_bpf group that Wireshark's ChmodBPF
sets up. See EtherCAT.Bus.Transport.Bpf. The raw simulator and redundant
rings are Linux only.
backend = {:raw, %{interface: "eth0"}}
{:ok, scan} = EtherCAT.Scan.scan(backend)
scan.discovered_slavesTo 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)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}
endList 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.
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. |
For applications using 0.4.x:
- Supervise the runtime yourself. Add
{EtherCAT.Runtime, []}to the host supervision tree; starting the:ethercatapplication no longer starts a master. - Keep the session.
EtherCAT.start/1returns{:ok, session}, not:ok. Pass it to runtime, provisioning, diagnostics, signal, and capture operations. A stopped session returns{:error, :stale_session}; it never follows a restart. - Use an explicit backend. Replace
interface: "eth0"withbackend: {:raw, %{interface: "eth0"}}. UDP and redundant raw configurations also useEtherCAT.Backend.
| 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.
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.exsThe 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.
With the Xcode command line tools and read/write access to /dev/bpf* (see
EtherCAT.Bus.Transport.Bpf), create an isolated virtual Ethernet pair:
sudo ifconfig feth0 create
sudo ifconfig feth1 create
sudo ifconfig feth1 peer feth0
sudo ifconfig feth0 up
sudo ifconfig feth1 up
ETHERCAT_BPF_INTERFACE=feth0 ETHERCAT_BPF_PEER_INTERFACE=feth1 mix ci
sudo ifconfig feth0 destroy
sudo ifconfig feth1 destroyDo not attach these interfaces to a bridge or use physical adapters. These tests
send raw frames and check actual receive filtering, VLANs, padding, no self-receive,
and helper lifecycle. CI provisions this pair automatically and runs the same
mix ci on Linux and macOS. Without the BPF environment variables, wire tests are
excluded; once configured, setup or helper errors fail the suite. Portable helper
build-security and port-liveness tests run on both platforms.
- Write a driver: callback contract and catalogue pattern.
- Configure a PREOP session or use SDOs:
EtherCAT.Provisioning. - Write outputs or subscribe to observations:
EtherCAT; signal/latch subscriptions:EtherCAT.Signals. - Build virtual devices or inject faults:
EtherCAT.Simulator. - Capture hardware:
iex -S mix ethercat.capture --interface eth0; seeEtherCAT.Capture. - Run hardware checks:
MIX_ENV=test mix run test/integration/hardware/scripts/scan.exs --interface eth0; see the hardware guide before running bench scripts. - Understand internals: architecture. Build the API reference for your checkout with
mix docs.