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 mix tasks ethercat.capture.ex
Raw

lib/mix/tasks/ethercat.capture.ex

defmodule Mix.Tasks.Ethercat.Capture do
@moduledoc """
Boot an interactive PREOP capture session against a live EtherCAT ring.
Typical usage:
iex -S mix ethercat.capture --interface eth0
Optional UDP transport arguments are supported for simulator-backed sessions:
iex -S mix ethercat.capture --transport udp --host 127.0.0.2 --bind-ip 127.0.0.1
"""
use Mix.Task
@shortdoc "Boot an interactive PREOP capture session"
@requirements ["app.start"]
@default_await_ms 10_000
@default_scan_poll_ms 100
@default_scan_stable_ms 1_000
@impl true
def run(args) do
opts = parse_args!(args)
ensure_session_available!()
case EtherCAT.start(build_start_opts(opts)) do
:ok ->
:ok
{:error, reason} ->
Mix.raise("failed to start EtherCAT capture session: #{inspect(reason)}")
end
case EtherCAT.await_running(Keyword.fetch!(opts, :await_ms)) do
:ok ->
print_session_banner(opts)
{:error, reason} ->
_ = EtherCAT.stop()
Mix.raise("capture session did not reach a usable PREOP state: #{inspect(reason)}")
end
end
defp parse_args!(args) do
{parsed, rest, invalid} =
OptionParser.parse(
args,
strict: [
interface: :string,
transport: :string,
host: :string,
bind_ip: :string,
port: :integer,
await_ms: :integer,
scan_poll_ms: :integer,
scan_stable_ms: :integer,
frame_timeout_ms: :integer
]
)
if rest != [] do
Mix.raise("unexpected positional arguments: #{Enum.join(rest, " ")}")
end
if invalid != [] do
Mix.raise("invalid capture options: #{inspect(invalid)}")
end
transport = parse_transport!(Keyword.get(parsed, :transport, "raw"))
parsed
|> Keyword.put(:transport, transport)
|> Keyword.put_new(:await_ms, @default_await_ms)
|> Keyword.put_new(:scan_poll_ms, @default_scan_poll_ms)
|> Keyword.put_new(:scan_stable_ms, @default_scan_stable_ms)
|> maybe_parse_ip(:host)
|> maybe_parse_ip(:bind_ip)
end
defp parse_transport!("raw"), do: :raw
defp parse_transport!("udp"), do: :udp
defp parse_transport!(other),
do: Mix.raise("unsupported transport #{inspect(other)}; use raw or udp")
defp maybe_parse_ip(opts, key) do
case Keyword.get(opts, key) do
nil ->
opts
ip ->
case :inet.parse_address(String.to_charlist(ip)) do
{:ok, parsed_ip} ->
Keyword.put(opts, key, parsed_ip)
{:error, _reason} ->
Mix.raise(
"invalid IP for --#{key |> Atom.to_string() |> String.replace("_", "-")}: #{ip}"
)
end
end
end
defp ensure_session_available! do
case EtherCAT.state() do
{:error, :not_started} ->
:ok
{:ok, :idle} ->
:ok
state ->
Mix.raise(
"EtherCAT session already running in state #{inspect(state)}; stop it before starting capture"
)
end
end
defp build_start_opts(opts) do
start_opts = [
dc: nil,
domains: [],
slaves: [],
scan_poll_ms: Keyword.fetch!(opts, :scan_poll_ms),
scan_stable_ms: Keyword.fetch!(opts, :scan_stable_ms)
]
start_opts =
case Keyword.get(opts, :frame_timeout_ms) do
timeout_ms when is_integer(timeout_ms) and timeout_ms > 0 ->
Keyword.put(start_opts, :frame_timeout_ms, timeout_ms)
_ ->
start_opts
end
case Keyword.fetch!(opts, :transport) do
:raw ->
case Keyword.get(opts, :interface) do
interface when is_binary(interface) ->
Keyword.put(start_opts, :interface, interface)
_ ->
Mix.raise("raw capture sessions require --interface")
end
:udp ->
start_opts
|> Keyword.put(:transport, :udp)
|> maybe_put(:host, Keyword.get(opts, :host))
|> maybe_put(:bind_ip, Keyword.get(opts, :bind_ip))
|> maybe_put(:port, Keyword.get(opts, :port))
|> maybe_put(:interface, Keyword.get(opts, :interface))
end
end
defp print_session_banner(opts) do
Mix.shell().info("")
Mix.shell().info("EtherCAT capture session ready")
Mix.shell().info("State: #{inspect(EtherCAT.state())}")
Mix.shell().info("Transport: #{transport_label(opts)}")
case EtherCAT.Capture.list_slaves() do
{:ok, slaves} ->
Mix.shell().info("Discovered slaves:")
Enum.each(slaves, fn slave ->
Mix.shell().info(
" #{inspect(slave.name)} station=0x#{Integer.to_string(slave.station, 16)} al_state=#{inspect(Map.get(slave, :al_state))} coe=#{inspect(Map.get(slave, :coe))}"
)
end)
{:error, reason} ->
Mix.shell().info("Failed to list slaves: #{inspect(reason)}")
end
Mix.shell().info("")
unless IEx.started?() do
Mix.shell().info("This task is most useful under IEx:")
Mix.shell().info(" iex -S mix ethercat.capture --interface eth0")
Mix.shell().info("")
end
EtherCAT.Capture.help()
end
defp transport_label(opts) do
case Keyword.fetch!(opts, :transport) do
:raw ->
Keyword.fetch!(opts, :interface)
:udp ->
host =
case Keyword.get(opts, :host) do
nil -> "255.255.255.255"
ip -> :inet.ntoa(ip) |> List.to_string()
end
port = Keyword.get(opts, :port, 0x88A4)
"#{host}:#{port}"
end
end
defp maybe_put(opts, _key, nil), do: opts
defp maybe_put(opts, key, value), do: Keyword.put(opts, key, value)
end