Packages

An elixir library for MAVLink, an application that enables communication with other systems using the MAVLink protocol over serial, UDP and TCP connections, and utility modules for performing common MAVLink commands and tasks with one or more remote vehicles.

Current section

Files

Jump to
xmavlink lib mavlink_util focus_manager.ex
Raw

lib/mavlink_util/focus_manager.ex

defmodule XMAVLink.Util.FocusManager do
@moduledoc """
Manage a protected ETS table representing the nominated MAV focus of
zero or more local PIDs. The API uses this to streamline iex sessions
by letting the user select a MAV to work with and transparently adding
{system_id, component_id} tuples to call arguments.
Pass `context: context` to read or write focus in a scoped utility table
namespace.
"""
use GenServer
require Logger
alias XMAVLink.Util.{Context, Tables}
defstruct monitors: %{},
sessions: %{},
context: nil,
table_prefix: nil,
tables: Tables.names()
# API
def start_link(state, opts \\ []) do
GenServer.start_link(__MODULE__, state, Keyword.put_new(opts, :name, __MODULE__))
end
def focus() do
self() |> focus()
end
def focus(opts) when is_list(opts), do: focus(self(), opts)
def focus(pid) when is_pid(pid), do: focus(pid, [])
def focus(system_id) when is_integer(system_id), do: focus(system_id, 1)
def focus(pid, opts) when is_pid(pid) do
sessions = Context.new(opts).tables.sessions
with :ok <- require_table(sessions),
[{^pid, scid}] <- :ets.lookup(sessions, pid) do
if pid == self() do
Logger.info("Vehicle #{format(scid)}")
else
Logger.info("Vehicle #{format(scid)} for #{inspect(pid)}")
end
{:ok, scid}
else
{:error, reason} ->
{:error, reason}
_ ->
Logger.warning("#{inspect(pid)} has no vehicle focus")
{:error, :not_focussed}
end
end
def focus(system_id, component_id) when is_integer(system_id) do
focus(system_id, component_id, [])
end
def focus(system_id, component_id, opts)
when is_integer(system_id) and is_integer(component_id) and is_list(opts) do
with pid when is_pid(pid) <- GenServer.whereis(__MODULE__),
{:ok, {system_id, component_id, _}} <-
GenServer.call(pid, {:focus, {system_id, component_id}, Context.new(opts)}) do
configure_iex_prompt(system_id, component_id)
else
nil -> {:error, :not_started}
{:error, reason} -> {:error, reason}
end
end
@impl true
def init(opts) do
opts = Map.new(opts)
context = Context.new(opts)
:ets.new(context.tables.sessions, [:named_table, :protected, {:read_concurrency, true}, :set])
{:ok,
struct(
__MODULE__,
opts
|> Map.put(:context, context)
|> Map.put(:table_prefix, context.table_prefix)
|> Map.put(:tables, context.tables)
)}
end
@impl true
def handle_call(
{:focus, scid = {system_id, component_id}, context},
{caller_pid, _},
state
) do
tables = context.tables
with :ok <- require_table(tables.systems),
:ok <- require_writable_table(tables.sessions),
mavlink_major_version when is_number(mavlink_major_version) <-
:ets.foldl(
fn {next_scid, %{mavlink_major_version: mmv}}, acc ->
if next_scid == scid do
mmv
else
acc
end
end,
0,
tables.systems
) do
if mavlink_major_version > 0 do
state =
put_focus_session(state, tables, caller_pid, {
system_id,
component_id,
mavlink_major_version
})
Logger.info("Set focus to #{format(scid)}")
{:reply, {:ok, {system_id, component_id, mavlink_major_version}}, state}
else
Logger.warning("No such vehicle #{format(scid)}")
{:reply, {:error, :no_such_mav}, state}
end
else
{:error, reason} -> {:reply, {:error, reason}, state}
_ -> {:reply, {:error, :no_mav_data}, state}
end
end
@impl true
def handle_info({:DOWN, ref, :process, _pid, _reason}, state) do
case Map.pop(state.monitors, ref) do
{nil, _monitors} ->
{:noreply, state}
{session_key, monitors} ->
{session, sessions} = Map.pop(state.sessions, session_key)
delete_session(session, session_pid(session_key))
{:noreply,
%{
state
| monitors: monitors,
sessions: sessions
}}
end
end
defp configure_iex_prompt(system_id, component_id) do
if Code.ensure_loaded?(IEx) and function_exported?(IEx, :configure, 1) and
Process.whereis(IEx.Config) do
apply(IEx, :configure, [
[default_prompt: "iex(%counter) vehicle #{system_id}.#{component_id}>"]
])
end
:ok
end
defp put_focus_session(state, tables, pid, scid) do
session_key = {pid, tables.sessions}
state = drop_focus_monitor(state, session_key)
ref = Process.monitor(pid)
:ets.insert(tables.sessions, {pid, scid})
%{
state
| monitors: Map.put(state.monitors, ref, session_key),
sessions: Map.put(state.sessions, session_key, {ref, tables.sessions})
}
end
defp drop_focus_monitor(state, session_key = {pid, _sessions_table}) do
case Map.pop(state.sessions, session_key) do
{nil, _sessions} ->
state
{{ref, sessions_table}, sessions} ->
Process.demonitor(ref, [:flush])
delete_session_table_entry(sessions_table, pid)
%{
state
| monitors: Map.delete(state.monitors, ref),
sessions: sessions
}
end
end
defp delete_session(nil, _pid), do: :ok
defp delete_session({_ref, sessions_table}, pid),
do: delete_session_table_entry(sessions_table, pid)
defp delete_session_table_entry(sessions_table, pid) do
if :ets.info(sessions_table) != :undefined do
:ets.delete(sessions_table, pid)
end
end
defp session_pid({pid, _sessions_table}), do: pid
defp format({s, c, _}), do: format({s, c})
defp format({s, c}), do: "#{s}.#{c}"
defp require_table(table) do
case :ets.info(table) do
:undefined -> {:error, :not_started}
_ -> :ok
end
end
defp require_writable_table(table) do
case :ets.info(table) do
:undefined ->
{:error, :not_started}
_ ->
if :ets.info(table, :owner) == self() or :ets.info(table, :protection) == :public do
:ok
else
{:error, :not_started}
end
end
end
end