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

lib/mavlink/parser.ex

defmodule XMAVLink.Parser do
@moduledoc """
Parse a mavlink xml file into an idiomatic Elixir representation:
%{
version: 2,
dialect: 0,
enums: [
%{
name: :mav_autopilot,
description: "Micro air vehicle...",
entries: [
%{
value: 0,
name: :mav_autopilot_generic, (use atoms for identifiers)
description: "Generic autopilot..."
params: [ (only used by commands)
%{
index: 0,
description: ""
},
... more entry params
]
},
... more enum entries
]
},
... more enums
],
messages: [
%{
id: 0,
name: "optical_flow",
description: "Optical flow...",
fields: [
%{
type: "uint16_t",
ordinality: 1,
name: "flow_x",
units: "dpixels", (note: string not atom)
description: "Flow in pixels..."
},
... more message fields
]
},
... more messages
]
}
"""
import Enum, only: [empty?: 1, reduce: 3, reverse: 1, map: 2, sort_by: 2, into: 3, filter: 2]
import List, only: [first: 1]
import Record, only: [defrecord: 2, extract: 2]
import Regex, only: [replace: 3]
import String, only: [to_integer: 1, downcase: 1, to_atom: 1, split: 3]
@xmerl_header "xmerl/include/xmerl.hrl"
defrecord :xmlElement, extract(:xmlElement, from_lib: @xmerl_header)
defrecord :xmlAttribute, extract(:xmlAttribute, from_lib: @xmerl_header)
defrecord :xmlText, extract(:xmlText, from_lib: @xmerl_header)
@spec parse_mavlink_xml(String.t()) ::
%{
version: integer,
dialect: integer,
enums: [enum_description],
messages: [message_description]
}
| {:error, :enoent}
def parse_mavlink_xml(path) do
parse_mavlink_xml(path, %{}) |> Map.values() |> combine_definitions
end
def parse_mavlink_xml(path, paths) do
case Map.has_key?(paths, path) do
true ->
# Don't include a file twice
paths
false ->
case :xmerl_scan.file(path) do
{defs, []} ->
# Recursively add new includes to paths
paths =
reduce(
:xmerl_xpath.string(~c"/mavlink/include/text()", defs) |> map(&extract_text/1),
paths,
fn next_include, acc ->
include_path = Path.dirname(path) <> "/" <> next_include
parse_mavlink_xml(include_path, acc)
end
)
# And add ourselves to paths if we're not already there through a circular dependency
version =
:xmerl_xpath.string(~c"/mavlink/version/text()", defs)
|> extract_text
|> nil_to_zero_string
Map.put_new(paths, path, %{
version: version,
dialect:
:xmerl_xpath.string(~c"/mavlink/dialect/text()", defs)
|> extract_text
|> nil_to_zero_string,
enums:
for(
enum <- :xmerl_xpath.string(~c"/mavlink/enums/enum", defs),
do: parse_enum(enum)
),
messages:
for(
msg <- :xmerl_xpath.string(~c"/mavlink/messages/message", defs),
do: parse_message(msg, version)
)
})
{:error, :enoent} ->
Map.put(paths, path, {:error, "File '#{path}' does not exist"})
end
end
end
# See https://mavlink.io/en/guide/xml_schema.html, mavparse.py merge_enums() and
# check_duplicates() for proper validation. If making changes to definitions test
# first with mavgen for now.
# TODO Handle missing includes without borking
def combine_definitions([single_def]) do
single_def
end
def combine_definitions([
%{
version: v1,
dialect: d1,
enums: e1,
messages: m1
},
%{
version: v2,
dialect: d2,
enums: e2,
messages: m2
}
| more_definitions
]) do
combine_definitions([
%{
# strings > nil
version: max(v1, v2),
dialect: max(d1, d2),
enums: merge_enums(e1, e2),
messages: sort_by(m1 ++ m2, & &1.id)
}
| more_definitions
])
end
def merge_enums(as, bs) do
a_index = into(as, %{}, fn enum -> {enum.name, enum} end)
b_index = into(bs, %{}, fn enum -> {enum.name, enum} end)
only_in_a =
for name <- filter(Map.keys(a_index), &(!Map.has_key?(b_index, &1))), do: a_index[name]
only_in_b =
for name <- filter(Map.keys(b_index), &(!Map.has_key?(a_index, &1))), do: b_index[name]
in_a_and_b =
for name <- filter(Map.keys(a_index), &Map.has_key?(b_index, &1)) do
%{
a_index[name]
| entries: sort_by(a_index[name].entries ++ b_index[name].entries, & &1.value)
}
end
sort_by(only_in_a ++ in_a_and_b ++ only_in_b, & &1.name)
end
@type enum_description :: %{
name: atom,
description: String.t(),
entries: [entry_description]
}
@spec parse_enum(tuple) :: enum_description
defp parse_enum(element) do
%{
name: :xmerl_xpath.string(~c"@name", element) |> extract_text |> downcase |> to_atom,
description:
:xmerl_xpath.string(~c"/enum/description/text()", element)
|> extract_text
|> nil_to_empty_string,
entries: for(entry <- :xmerl_xpath.string(~c"/enum/entry", element), do: parse_entry(entry))
}
end
@type entry_description :: %{
value: integer | nil,
name: atom,
description: String.t(),
params: [param_description]
}
@spec parse_entry(tuple) :: entry_description
defp parse_entry(element) do
# Apparently optional in common.xml?
value_attr = :xmerl_xpath.string(~c"@value", element)
%{
value: if(not empty?(value_attr), do: extract_text(value_attr) |> to_integer, else: nil),
name: :xmerl_xpath.string(~c"@name", element) |> extract_text |> downcase |> to_atom,
description:
:xmerl_xpath.string(~c"/entry/description/text()", element)
|> extract_text
|> nil_to_empty_string,
params: for(param <- :xmerl_xpath.string(~c"/entry/param", element), do: parse_param(param))
}
end
@type param_description :: %{
index: integer,
description: String.t()
}
@spec parse_param(tuple) :: param_description
defp parse_param(element) do
%{
index: :xmerl_xpath.string(~c"@index", element) |> extract_text |> to_integer,
description: :xmerl_xpath.string(~c"/param/text()", element) |> extract_text
}
end
@type message_description :: %{
id: integer,
name: String.t(),
description: String.t(),
has_ext_fields: boolean,
fields: [field_description]
}
@spec parse_message(tuple, String.t()) :: message_description
defp parse_message(element, version) do
message_description =
reduce(
xmlElement(element, :content),
%{
id: :xmerl_xpath.string(~c"@id", element) |> extract_text |> to_integer,
name: :xmerl_xpath.string(~c"@name", element) |> extract_text,
description:
:xmerl_xpath.string(~c"/message/description/text()", element) |> extract_text,
has_ext_fields: false,
fields: []
},
fn next_child, acc ->
case xmlElement(next_child, :name) do
:field ->
%{acc | fields: [parse_field(next_child, version, acc.has_ext_fields) | acc.fields]}
:extensions ->
%{acc | has_ext_fields: true}
_ ->
acc
end
end
)
%{message_description | fields: reverse(message_description.fields)}
end
@type field_description :: %{
type: String.t(),
ordinality: integer,
omit_arg: boolean,
is_extension: boolean,
constant_val: String.t() | nil,
name: String.t(),
enum: String.t(),
display: :bitmask | nil,
print_format: String.t() | nil,
units: atom | nil,
description: String.t()
}
@spec parse_field(tuple, binary(), boolean) :: field_description
defp parse_field(element, version, is_extension_field) do
{type, ordinality, omit_arg, constant_val} =
:xmerl_xpath.string(~c"@type", element)
|> extract_text
|> parse_type_ordinality_omit_arg_constant_val(version)
%{
type: type,
ordinality: ordinality,
omit_arg: omit_arg,
is_extension: is_extension_field,
constant_val: constant_val,
# You can't downcase this, wrecks crc_extra calc for POWER_STATUS
name: :xmerl_xpath.string(~c"@name", element) |> extract_text,
enum:
:xmerl_xpath.string(~c"@enum", element) |> extract_text |> nil_to_empty_string |> downcase,
display: :xmerl_xpath.string(~c"@display", element) |> extract_text |> to_atom_or_nil,
print_format: :xmerl_xpath.string(~c"@print_format", element) |> extract_text,
units: :xmerl_xpath.string(~c"@units", element) |> extract_text |> to_atom_or_nil,
description:
:xmerl_xpath.string(~c"/field/text()", element) |> extract_text |> nil_to_empty_string
}
end
@spec parse_type_ordinality_omit_arg_constant_val(String.t(), String.t()) ::
{String.t(), integer, boolean, String.t() | nil}
defp parse_type_ordinality_omit_arg_constant_val(type_string, version) do
[type | ordinality] =
type_string
|> split(["[", "]"], trim: true)
case type do
"uint8_t_mavlink_version" ->
{"uint8_t", 1, true, version}
_ ->
{
type,
cond do
ordinality |> empty? ->
1
true ->
ordinality |> first |> to_integer
end,
false,
nil
}
end
end
# TODO Can't spec this without causing dialyzer "nil can't match binary" - Erlang types?
defp extract_text([xml]), do: extract_text(xml)
defp extract_text(xmlText(value: value)), do: clean_string(value)
defp extract_text(xmlAttribute(value: value)), do: clean_string(value)
defp extract_text(_), do: nil
@spec clean_string([char] | binary) :: String.t()
defp clean_string(s) do
trimmed = s |> List.to_string() |> String.trim() |> String.replace("\"", "'")
replace(~r/\s+/, trimmed, " ")
end
@spec nil_to_empty_string(String.t() | nil) :: String.t()
defp nil_to_empty_string(nil), do: ""
defp nil_to_empty_string(value) when is_binary(value), do: value
@spec nil_to_zero_string(String.t() | nil) :: String.t()
defp nil_to_zero_string(nil), do: "0"
defp nil_to_zero_string(value) when is_binary(value), do: value
@spec to_atom_or_nil(String.t() | nil) :: atom | nil
defp to_atom_or_nil(nil), do: nil
defp to_atom_or_nil(""), do: nil
defp to_atom_or_nil(value) when is_binary(value), do: to_atom(value)
end