Public protocol/device driver boundary for EtherCAT slaves.
A driver describes protocol-facing concerns only:
- device identity
- logical PDO signal layout
- signal encoding and decoding
- static signal metadata
Semantic state projection, machine commands, and machine events do not belong
in this behaviour. Those concerns should be implemented above EtherCAT by a
semantic integration such as an Entity.Provider adapter.
Specialist protocol concerns live on separate behaviours:
EtherCAT.Driver.Provisioningfor mailbox startup/setup stepsEtherCAT.Simulator.Adapterfor simulator-side companion definitions
Latch events are consumed through EtherCAT.Signals subscriptions.
Codec contract
Codec callbacks run inside the slave process and must be fast, pure functions.
Return {:error, reason} for invalid values or undecodable data. Exceptions
indicate driver bugs and are not converted to successful observations.
Codecs receive or return byte-aligned binaries of ceil(bit_size / 8) bytes.
Fields use EtherCAT little-endian bit order: bit zero is the least significant
bit of the first byte. Unused high bits of the last byte must be zero. Signed
and multi-byte values must be encoded explicitly by the driver.
Implement only the codecs needed by the registered signal directions. The runtime checks those capabilities during PREOP configuration.
describe/1 supplies static metadata only; when absent, endpoints are empty.
signal_model/2 receives discovered PDOs and is never called to build a
description. Return named EtherCAT.Driver.Signal structs for all mappings.
Concrete device drivers are normally application-owned. This library ships the contract and a generic default driver, while sample device-specific drivers live in test support only.
Share signal declarations
Keep related device knowledge together without changing the callbacks. A private catalogue can pair each endpoint's metadata with its PDO mapping, then project the two views independently. For a device with two known one-bit input PDOs:
defmodule MyApp.DigitalInputs do
@behaviour EtherCAT.Driver
alias EtherCAT.Driver.Signal
alias EtherCAT.Endpoint
@signals [
{%Endpoint{signal: :ch1, direction: :input, type: :boolean},
Signal.whole_pdo(0x1A00)},
{%Endpoint{signal: :ch2, direction: :input, type: :boolean},
Signal.whole_pdo(0x1A01)}
]
@impl true
def signal_model(_config, _pdos) do
Enum.map(@signals, fn {endpoint, mapping} -> {endpoint.signal, mapping} end)
end
@impl true
def describe(_config) do
%{device_type: :digital_input, endpoints: Enum.map(@signals, &elem(&1, 0))}
end
@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}
endFor configurable devices, select the enabled rows in one private
catalogue(config) function used by both callbacks. Descriptions must still
work without discovery. Resolve discovery-dependent mappings in
signal_model/2, not in describe/1, and do not silently omit an expected
signal just because its PDO is missing: leave the mismatch visible to the
runtime's mapping validation.
This is an optional authoring pattern, not a new schema or codec DSL. Scalar types, bit widths, and discovered directions must still agree. If codecs look up catalogue rows on every call, measure that cost for your cycle budget; explicit codec clauses remain appropriate.
The repository includes a
configurable mixed-PDO example,
a before/after comparison,
and trade-offs.
Run the comparison from a checkout with
mix test test/ethercat/driver/catalogue_example_test.exs.
Summary
Callbacks
Decode one input signal's protocol bytes into an application value.
Describe the configured device type and static signal endpoints without bus access.
Encode one output signal value into its protocol bytes without transmitting it.
Declare the driver's vendor ID, product code, and optional revision.
Map logical signal names to whole PDOs or bit slices within discovered PDOs.
Types
@type config() :: map()
@type description() :: %{ optional(:device_type) => atom(), optional(:endpoints) => [EtherCAT.Endpoint.t() | map()] }
@type identity() :: %{ :vendor_id => non_neg_integer(), :product_code => non_neg_integer(), optional(:revision) => non_neg_integer() | :any }
@type signal_name() :: atom()
Callbacks
@callback decode_signal(signal_name(), config(), binary()) :: {:ok, term()} | {:error, term()}
Decode one input signal's protocol bytes into an application value.
Required only when registered signals include inputs. Return {:error, reason}
for undecodable data rather than substituting a value. This converts an existing
observation; it does not read from the bus.
@callback describe(config()) :: description()
Describe the configured device type and static signal endpoints without bus access.
Optional; missing metadata defaults to no device type and an empty endpoint
list. Use the same signal names as signal_model/2, but do not require
discovery or live values to build the description.
@callback encode_signal(signal_name(), config(), term()) :: {:ok, binary()} | {:error, term()}
Encode one output signal value into its protocol bytes without transmitting it.
Required only when registered signals include outputs. Follow the module's
byte-width, padding, and little-endian contract. Return {:error, reason} for
invalid values; failed encoding leaves the staged output unchanged.
@callback identity() :: identity() | nil
Declare the driver's vendor ID, product code, and optional revision.
Omission or nil means no declared identity. identity/1 defaults an omitted
revision to :any. Simulator hydration uses this declaration for identity
defaults; it does not by itself enforce a hardware compatibility check.
@callback signal_model(config(), pdos :: [EtherCAT.Driver.PDO.t()]) :: [ {signal_name(), EtherCAT.Driver.Signal.t()} ]
Map logical signal names to whole PDOs or bit slices within discovered PDOs.
This is the only mandatory callback. Use the discovered pdos when the
device's layout requires resolution; this callback declares mappings, not
mailbox setup or live values. The runtime validates requested mappings against
discovery before registering process data.