Current section
Files
Jump to
Current section
Files
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
: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