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 heartbeat.ex
Raw

lib/mavlink/heartbeat.ex

defmodule XMAVLink.Heartbeat do
@moduledoc """
Emits MAVLink HEARTBEAT messages on a configurable interval.
Most MAVLink nodes (cameras, GCSes, companion computers, autopilots)
must emit a HEARTBEAT roughly once per second so peers know they're
alive. Without it, dynamic / peer-learning routers like the
reference `mavlink-router` don't forward traffic to them. xmavlink
itself does not emit HEARTBEATs by default; consumers traditionally
built and sent their own. This module standardises that pattern.
## Configuration
Set `:heartbeat` in the application environment for `:xmavlink`. If
the key is unset or `nil`, no HEARTBEATs are emitted (backwards
compatible with versions ≤ 0.6.0).
### Static message
config :xmavlink,
heartbeat: [
interval_ms: 1000,
source_system: 245,
source_component: 191,
message: %Common.Message.Heartbeat{
type: :mav_type_gcs,
autopilot: :mav_autopilot_invalid,
base_mode: MapSet.new(),
custom_mode: 0,
system_status: :mav_state_active,
mavlink_version: 3
}
]
Suitable for nodes whose HEARTBEAT contents don't change at runtime
(e.g. a stateless GCS).
### Dynamic builder
config :xmavlink,
heartbeat: [
interval_ms: 1000,
source_system: 1,
source_component: 100,
builder: {MyApp.Mavlink, :build_heartbeat, []}
]
The `{module, function, args}` tuple is invoked on every tick to
produce a fresh struct. Use this when `system_status`, `base_mode`,
or `custom_mode` should reflect application state.
Either `:message` or `:builder` is required (not both). `:interval_ms`
is required.
## First heartbeat
The first HEARTBEAT is dispatched immediately after init, so a
peer-learning router admits the node within milliseconds rather than
waiting up to a full interval.
"""
use GenServer
require Logger
@doc false
def start_link(spec) do
{name, spec} = Keyword.pop(spec, :name, __MODULE__)
opts = if name, do: [name: name], else: []
GenServer.start_link(__MODULE__, spec, opts)
end
@impl true
def init(spec) do
interval_ms = Keyword.fetch!(spec, :interval_ms)
builder = build_builder(spec)
version = Keyword.get(spec, :version, 2)
send_opts = Keyword.take(spec, [:source_system, :source_component])
# First heartbeat ASAP so peer-learning routers admit us fast.
send(self(), :tick)
{:ok, %{interval_ms: interval_ms, builder: builder, version: version, send_opts: send_opts}}
end
@impl true
def handle_info(:tick, state) do
case safe_build(state.builder) do
{:ok, msg} ->
XMAVLink.Router.pack_and_send(msg, state.version, state.send_opts)
{:error, error} ->
Logger.error("XMAVLink.Heartbeat builder failed: #{inspect(error)}")
end
Process.send_after(self(), :tick, state.interval_ms)
{:noreply, state}
end
defp build_builder(spec) do
message = Keyword.get(spec, :message)
builder_mfa = Keyword.get(spec, :builder)
cond do
message != nil and builder_mfa != nil ->
raise ArgumentError,
"XMAVLink heartbeat config: provide either :message or :builder, not both"
message != nil ->
fn -> message end
builder_mfa != nil ->
normalize_builder(builder_mfa)
true ->
raise ArgumentError,
"XMAVLink heartbeat config must include either :message or :builder"
end
end
defp normalize_builder({m, f, a}) when is_atom(m) and is_atom(f) and is_list(a) do
fn -> apply(m, f, a) end
end
defp normalize_builder(fun) when is_function(fun, 0), do: fun
defp normalize_builder(other) do
raise ArgumentError,
"XMAVLink heartbeat :builder must be a {module, function, args} tuple " <>
"or a 0-arity function, got: #{inspect(other)}"
end
defp safe_build(builder) do
{:ok, builder.()}
rescue
error -> {:error, error}
end
end