Packages

Pure-Elixir EtherCAT master built on OTP. Declarative bus configuration, cyclic process data exchange, CoE SDO transfers, and distributed clocks.

Current section

Files

Jump to
ethercat lib ethercat master config.ex
Raw

lib/ethercat/master/config.ex

defmodule EtherCAT.Master.Config do
@moduledoc false
alias EtherCAT.DC.Config, as: DCConfig
alias EtherCAT.Master.Config.Domain
alias EtherCAT.Master.Config.DomainPlan
alias EtherCAT.Master.Config.Slave
alias EtherCAT.Slave.Config, as: SlaveConfig
@default_base_station 0x1000
@max_station_address 0xFFFF
@master_option_keys [
:slaves,
:domains,
:base_station,
:dc,
:dc_cycle_ns,
:frame_timeout_ms,
:scan_stable_ms,
:scan_poll_ms
]
@type t :: %__MODULE__{
base_station: non_neg_integer(),
bus_opts: keyword(),
dc_config: DCConfig.t() | nil,
domain_config: [DomainPlan.t()],
slave_config: [SlaveConfig.t()],
frame_timeout_override_ms: pos_integer() | nil,
scan_stable_ms: pos_integer(),
scan_poll_ms: pos_integer()
}
defstruct base_station: @default_base_station,
bus_opts: [],
dc_config: %DCConfig{},
domain_config: [],
slave_config: [],
frame_timeout_override_ms: nil,
scan_stable_ms: 1_000,
scan_poll_ms: 100
@spec normalize_start_options(term()) :: {:ok, t()} | {:error, term()}
def normalize_start_options(opts) when is_list(opts) do
slave_config = Keyword.get(opts, :slaves, [])
domain_config = Keyword.get(opts, :domains, [])
base_station = Keyword.get(opts, :base_station, @default_base_station)
dc = Keyword.get(opts, :dc, %DCConfig{})
frame_timeout_override_ms = Keyword.get(opts, :frame_timeout_ms)
scan_stable_ms = Keyword.get(opts, :scan_stable_ms, 1_000)
scan_poll_ms = Keyword.get(opts, :scan_poll_ms, 100)
with :ok <- validate_bus_source(opts),
:ok <- reject_legacy_start_options(opts),
:ok <- validate_base_station(base_station),
{:ok, dc_config} <- normalize_dc_config(dc),
:ok <- validate_frame_timeout_override_ms(frame_timeout_override_ms),
{:ok, normalized_domains} <- Domain.normalize_configs(domain_config),
{:ok, allocated_domains} <- Domain.allocate_logical_bases(normalized_domains),
{:ok, normalized_slaves} <- Slave.normalize_configs(slave_config),
:ok <- validate_scan_opts(scan_stable_ms, scan_poll_ms) do
{:ok,
%__MODULE__{
base_station: base_station,
bus_opts: build_bus_start_opts(opts, frame_timeout_override_ms),
dc_config: dc_config,
domain_config: allocated_domains,
slave_config: normalized_slaves,
frame_timeout_override_ms: frame_timeout_override_ms,
scan_stable_ms: scan_stable_ms,
scan_poll_ms: scan_poll_ms
}}
else
{:error, _} = err -> err
end
end
def normalize_start_options(_opts), do: {:error, :invalid_start_options}
@spec normalize_runtime_slave_config(atom(), term(), SlaveConfig.t()) ::
{:ok, SlaveConfig.t()} | {:error, term()}
def normalize_runtime_slave_config(slave_name, spec, current_config),
do: Slave.normalize_runtime_config(slave_name, spec, current_config)
@spec effective_slave_config([SlaveConfig.t()], non_neg_integer()) ::
{:ok, [SlaveConfig.t()]} | {:error, term()}
def effective_slave_config(slave_config, bus_count),
do: Slave.effective_config(slave_config, bus_count)
@spec domain_ids([DomainPlan.t()]) :: [atom()]
def domain_ids(domain_config), do: Domain.ids(domain_config)
@spec unknown_domain_ids([DomainPlan.t()], SlaveConfig.t()) :: [atom()]
def unknown_domain_ids(domain_config, %SlaveConfig{} = slave_config) do
known_domains = MapSet.new(Domain.ids(domain_config))
slave_config
|> Slave.requested_domain_ids()
|> Enum.reject(&MapSet.member?(known_domains, &1))
end
@spec activatable_slave_names([SlaveConfig.t()]) :: [atom()]
def activatable_slave_names(slave_config), do: Slave.activatable_names(slave_config)
@spec requested_domain_ids(SlaveConfig.t()) :: [atom()]
def requested_domain_ids(%SlaveConfig{} = slave_config),
do: Slave.requested_domain_ids(slave_config)
@spec fetch_slave_config([SlaveConfig.t()], atom()) ::
{:ok, SlaveConfig.t(), non_neg_integer()} | {:error, term()}
def fetch_slave_config(slave_config, slave_name),
do: Slave.fetch_config(slave_config, slave_name)
@spec local_config_changed?(SlaveConfig.t(), SlaveConfig.t()) :: boolean()
def local_config_changed?(%SlaveConfig{} = current_config, %SlaveConfig{} = updated_config),
do: Slave.local_config_changed?(current_config, updated_config)
@spec domain_start_opts(DomainPlan.t()) :: keyword()
def domain_start_opts(%DomainPlan{} = config), do: Domain.start_opts(config)
defp build_bus_start_opts(opts, frame_timeout_override_ms) do
opts
|> Keyword.drop(@master_option_keys)
|> Keyword.put_new(:name, EtherCAT.Bus)
|> maybe_put_frame_timeout(frame_timeout_override_ms)
end
defp validate_bus_source(opts) do
cond do
udp_transport?(opts) ->
validate_udp_bus_source(opts)
true ->
case Keyword.fetch(opts, :interface) do
{:ok, _interface} -> :ok
:error -> {:error, :missing_interface}
end
end
end
defp validate_base_station(base_station)
when is_integer(base_station) and base_station >= 0 and
base_station <= @max_station_address,
do: :ok
defp validate_base_station(_base_station),
do: {:error, {:invalid_start_options, :invalid_base_station}}
defp reject_legacy_start_options(opts) do
if Keyword.has_key?(opts, :dc_cycle_ns) do
{:error, {:invalid_start_options, :legacy_dc_cycle_ns}}
else
:ok
end
end
defp normalize_dc_config(nil), do: {:ok, nil}
defp normalize_dc_config(%DCConfig{} = dc_config), do: validate_dc_config(dc_config)
defp normalize_dc_config(opts) when is_list(opts) do
validate_dc_config(%DCConfig{
cycle_ns: Keyword.get(opts, :cycle_ns, 1_000_000),
await_lock?: Keyword.get(opts, :await_lock?, false),
lock_policy: Keyword.get(opts, :lock_policy, :advisory),
lock_threshold_ns: Keyword.get(opts, :lock_threshold_ns, 100),
lock_timeout_ms: Keyword.get(opts, :lock_timeout_ms, 5_000),
warmup_cycles: Keyword.get(opts, :warmup_cycles, 0)
})
end
defp normalize_dc_config(_dc_config), do: {:error, {:invalid_start_options, :invalid_dc}}
defp validate_dc_config(%DCConfig{} = dc_config)
when is_integer(dc_config.cycle_ns) and dc_config.cycle_ns >= 1_000_000 and
rem(dc_config.cycle_ns, 1_000_000) == 0 and
is_boolean(dc_config.await_lock?) and
dc_config.lock_policy in [:advisory, :recovering, :fatal] and
is_integer(dc_config.lock_threshold_ns) and dc_config.lock_threshold_ns > 0 and
is_integer(dc_config.lock_timeout_ms) and dc_config.lock_timeout_ms > 0 and
is_integer(dc_config.warmup_cycles) and dc_config.warmup_cycles >= 0 do
{:ok, dc_config}
end
defp validate_dc_config(_dc_config), do: {:error, {:invalid_start_options, :invalid_dc}}
defp validate_frame_timeout_override_ms(nil), do: :ok
defp validate_frame_timeout_override_ms(timeout_ms)
when is_integer(timeout_ms) and timeout_ms > 0,
do: :ok
defp validate_frame_timeout_override_ms(_timeout_ms),
do: {:error, {:invalid_start_options, :invalid_frame_timeout_ms}}
defp validate_scan_opts(scan_stable_ms, scan_poll_ms)
when is_integer(scan_stable_ms) and scan_stable_ms >= 0 and
is_integer(scan_poll_ms) and scan_poll_ms > 0,
do: :ok
defp validate_scan_opts(_scan_stable_ms, _scan_poll_ms),
do: {:error, {:invalid_start_options, :invalid_scan_options}}
defp maybe_put_frame_timeout(opts, timeout_ms) when is_integer(timeout_ms) and timeout_ms > 0 do
Keyword.put(opts, :frame_timeout_ms, timeout_ms)
end
defp maybe_put_frame_timeout(opts, _timeout_ms), do: opts
defp validate_udp_bus_source(opts) do
cond do
Keyword.has_key?(opts, :interface) ->
:ok
Keyword.has_key?(opts, :host) ->
:ok
true ->
{:error, {:invalid_start_options, :missing_udp_host}}
end
end
defp udp_transport?(opts) do
case {Keyword.get(opts, :transport), Keyword.get(opts, :transport_mod)} do
{:udp, _} ->
true
{_, EtherCAT.Bus.Transport.UdpSocket} ->
true
_ ->
false
end
end
end