# Xmavlink case required for `mix xmavlink ...` to work defmodule Mix.Tasks.Xmavlink do use Mix.Task import XMAVLink.Parser import DateTime import Enum, only: [any?: 2, count: 1, join: 2, map: 2, filter: 2, reduce: 3, reverse: 1, sort: 1, into: 3] import String, only: [trim: 1, replace: 3, split: 2, capitalize: 1, downcase: 1] import XMAVLink.Utils import Mix.Generator, only: [create_file: 3] import Path, only: [rootname: 1, basename: 1] import Bitwise @doc """ """ @shortdoc "Generate Elixir Module from MAVLink dialect XML" @spec run([String.t()]) :: :ok @impl Mix.Task def run([dialect_xml_path]) do run([dialect_xml_path, "#{dialect_xml_path |> rootname |> basename}.ex"]) end def run([dialect_xml_path, output_ex_source_path]) do run([ dialect_xml_path, output_ex_source_path, dialect_xml_path |> rootname |> basename |> module_case ]) end def run([dialect_xml_path, output_ex_source_path, module_name]) do case parse_mavlink_xml(dialect_xml_path) do {:error, message} -> IO.puts(message) %{version: version, dialect: dialect, enums: enums, messages: messages} -> enum_code_fragments = get_enum_code_fragments(enums, module_name) message_code_fragments = get_message_code_fragments(messages, enums, module_name) unit_code_fragments = get_unit_code_fragments(messages) true = create_file( output_ex_source_path, """ defmodule #{module_name}.Types do @typedoc "A MAVLink message" @type message :: #{map(messages, &"#{module_name}.Message.#{&1[:name] |> module_case}") |> join(" | ")} @typedoc "An atom representing a MAVLink enumeration type" @type enum_type :: #{map(enums, &":#{&1[:name]}") |> join(" | ")} @typedoc "An atom representing a MAVLink enumeration type value" @type enum_value :: #{map(enums, &"#{&1[:name]}") |> join(" | ")} @typedoc "Measurement unit of field value" @type field_unit :: #{unit_code_fragments |> join(~s( | )) |> trim} #{enum_code_fragments |> map(& &1[:type]) |> join("\n\n ")} end #{message_code_fragments |> map(& &1.module) |> join("\n\n") |> trim} defmodule #{module_name} do import String, only: [replace_trailing: 3] import XMAVLink.Utils, only: [unpack_array: 2, unpack_float: 1] import Bitwise @moduledoc ~s(#{module_name} #{version}.#{dialect} generated by MAVLink mix task from #{dialect_xml_path} on #{utc_now()}) @doc "MAVLink version" @spec mavlink_version() :: #{version} def mavlink_version(), do: #{version} @doc "MAVLink dialect" @spec mavlink_dialect() :: #{dialect} def mavlink_dialect(), do: #{dialect} @doc "Return a String description of a MAVLink enumeration" @spec describe(#{module_name}.Types.enum_type | #{module_name}.Types.enum_value) :: String.t #{enum_code_fragments |> map(& &1[:describe]) |> join("\n ") |> trim} @doc "Return keyword list of mav_cmd parameters" @spec describe_params(#{module_name}.Types.mav_cmd) :: XMAVLink.Types.param_description_list #{enum_code_fragments |> map(& &1[:describe_params]) |> join("\n ") |> trim} @doc "Return encoded integer value used in a MAVLink message for an enumeration value" #{enum_code_fragments |> map(& &1[:encode_spec]) |> join("\n ") |> trim} #{enum_code_fragments |> map(& &1[:encode]) |> join("\n ") |> trim} @doc "Return the atom representation of a MAVLink enumeration value from the enumeration type and encoded integer" #{enum_code_fragments |> map(& &1[:decode_spec]) |> join("\n ") |> trim} #{enum_code_fragments |> map(& &1[:decode]) |> join("\n ") |> trim} def decode(value, _enum), do: value @doc "Return the message checksum and size in bytes for a message with a specified id" @typep target_type :: :broadcast | :system | :system_component | :component @spec msg_attributes(XMAVLink.Types.message_id) :: {:ok, XMAVLink.Types.crc_extra, pos_integer, target_type} | {:error, :unknown_message_id} #{message_code_fragments |> map(& &1.msg_attributes) |> join("") |> trim} def msg_attributes(_), do: {:error, :unknown_message_id} @doc "Helper function for messages to pack bitmask fields" @spec pack_bitmask(MapSet.t(#{module_name}.Types.enum_value), #{module_name}.Types.enum_type, (#{module_name}.Types.enum_value, #{module_name}.Types.enum_type -> integer)) :: integer def pack_bitmask(flag_set, enum, encode), do: Enum.reduce(flag_set, 0, & Bitwise.bxor(&2, encode.(&1, enum))) @doc "Helper function for decode() to unpack bitmask fields" @spec unpack_bitmask(integer, #{module_name}.Types.enum_type, (integer, #{module_name}.Types.enum_type -> #{module_name}.Types.enum_value), MapSet.t, integer) :: MapSet.t(#{module_name}.Types.enum_value) def unpack_bitmask(value, enum, decode, acc \\\\ MapSet.new(), pos \\\\ 1) do case {decode.(pos, enum), (value &&& pos) != 0} do {not_atom, _} when not is_atom(not_atom) -> acc {entry, true} -> unpack_bitmask(value, enum, decode, MapSet.put(acc, entry), pos <<< 1) {_, false} -> unpack_bitmask(value, enum, decode, acc, pos <<< 1) end end @doc "Unpack a MAVLink message given a MAVLink frame's message id and payload" @spec unpack(XMAVLink.Types.message_id, binary) :: #{module_name}.Types.message | {:error, :unknown_message} #{message_code_fragments |> map(& &1.unpack) |> join("") |> trim} def unpack(_, _), do: {:error, :unknown_message} end """, [] ) IO.puts("Generated #{module_name} in '#{output_ex_source_path}'.") :ok end end @type enum_detail :: %{ type: String.t(), describe: String.t(), describe_params: String.t(), encode: String.t(), decode: String.t() } @spec get_enum_code_fragments([XMAVLink.Parser.enum_description()], String.t()) :: [enum_detail] defp get_enum_code_fragments(enums, module_name) do for enum <- enums do %{ name: name, description: description } = enum entry_code_fragments = get_entry_code_fragments(enum) %{ type: ~s/@typedoc "#{description}"\n / <> ~s/@type #{name} :: / <> (map(entry_code_fragments, &":#{&1[:name]}") |> join(" | ")), describe: ~s/def describe(:#{name}), do: "#{escape(description)}"\n / <> (map(entry_code_fragments, & &1[:describe]) |> join("\n ")), describe_params: filter(entry_code_fragments, &(&1 != nil)) |> map(& &1[:describe_params]) |> join("\n "), encode_spec: "@spec encode(#{module_name}.Types.#{name}, :#{name}) :: " <> (map(entry_code_fragments, & &1[:value]) |> join(" | ")), encode: map(entry_code_fragments, & &1[:encode]) |> join("\n "), decode_spec: "@spec decode(" <> (map(entry_code_fragments, & &1[:value]) |> join(" | ")) <> ", :#{name}) :: #{module_name}.Types.#{name}", decode: map(entry_code_fragments, & &1[:decode]) |> join("\n ") } end end @type entry_detail :: %{ name: String.t(), describe: String.t(), describe_params: String.t(), encode: String.t(), decode: String.t() } @spec get_entry_code_fragments(XMAVLink.Parser.enum_description()) :: [entry_detail] defp get_entry_code_fragments(enum = %{name: enum_name, entries: entries}) do bitmask? = looks_like_a_bitmask?(enum) {details, _} = reduce( entries, {[], 0}, fn entry, {details, next_value} -> %{ name: entry_name, description: entry_description, value: entry_value, params: entry_params } = entry # Use provided value or continue monotonically from last value: in common.xml MAV_STATE uses this {entry_value, next_value} = case entry_value do nil -> {next_value, next_value + 1} _ -> {entry_value, entry_value + 1} end entry_value_string = if bitmask?, do: "0b#{Integer.to_string(entry_value, 2)}", else: Integer.to_string(entry_value) { [ %{ name: entry_name, describe: ~s/def describe(:#{entry_name}), do: "#{escape(entry_description)}"/, describe_params: get_param_code_fragments(entry_name, entry_params), encode: ~s/def encode(:#{entry_name}, :#{enum_name}), do: #{entry_value_string}/, decode: ~s/def decode(#{entry_value_string}, :#{enum_name}), do: :#{entry_name}/, value: entry_value_string } | details ], next_value } end ) reverse(details) end @spec get_param_code_fragments(String.t(), [XMAVLink.Parser.param_description()]) :: String.t() defp get_param_code_fragments(entry_name, entry_params) do cond do count(entry_params) == 0 -> nil true -> ~s/def describe_params(:#{entry_name}), do: [/ <> (map(entry_params, &~s/{#{&1[:index]}, "#{&1[:description]}"}/) |> join(", ")) <> ~s/]/ end end @spec get_message_code_fragments( [XMAVLink.Parser.message_description()], [enum_detail], String.t() ) :: [String.t()] defp get_message_code_fragments(messages, enums, module_name) do # Lookup used by looks_like_a_bitmask?() enums_by_name = into(enums, %{}, fn enum -> {Atom.to_string(enum.name), enum} end) for message <- messages do message_module_name = message.name |> module_case enforce_field_names = Enum.filter(message.fields, &(!&1.is_extension)) |> map(&(":" <> downcase(&1.name))) |> join(", ") field_names = message.fields |> map(&(":" <> downcase(&1.name))) |> join(", ") field_types = message.fields |> map(&(downcase(&1.name) <> ": " <> field_type(&1, module_name))) |> join(", ") wire_order = message.fields |> wire_order target = case {any?(message.fields, &(&1.name == "target_system")), any?(message.fields, &(&1.name == "target_component"))} do {false, false} -> :broadcast {true, false} -> :system {true, true} -> :system_component {false, true} -> # Does this happen? :component end # Have to append "_f" to stop clash with reserved elixir words like "end" [unpack_binary_pattern, unpack_binary_pattern_ext] = for field_list <- wire_order do field_list |> map( &(downcase(&1.name) <> "_f::" <> if(&1.ordinality > 1, do: "binary-size(#{type_to_binary(&1.type).size * &1.ordinality})", else: type_to_binary(&1.type).pattern )) ) |> join(",") end [unpack_struct_fields, unpack_struct_fields_ext] = for field_list <- wire_order do field_list |> map(&(downcase(&1.name) <> ": " <> unpack_field_code_fragment(&1, enums_by_name))) |> join(", ") end [pack_binary_pattern, pack_binary_pattern_ext] = for field_list <- wire_order do field_list |> map(&pack_field_code_fragment(&1, enums_by_name, module_name)) |> join(",") end crc_extra = calculate_message_crc_extra(message) # Including extension fields - currently only used for MAVLink 2 payload truncation expected_payload_size = reduce( message.fields, 0, # Before MAVLink 2 trailing 0 truncation fn field, sum -> sum + type_to_binary(field.type).size * field.ordinality end ) if message.has_ext_fields do %{ msg_attributes: """ def msg_attributes(#{message.id}), do: {:ok, #{crc_extra}, #{expected_payload_size}, :#{target}} """, unpack: """ def unpack(#{message.id}, 1, <<#{unpack_binary_pattern}>>), do: {:ok, %#{module_name}.Message.#{message_module_name}{#{unpack_struct_fields}}} def unpack(#{message.id}, 2, <<#{unpack_binary_pattern},#{unpack_binary_pattern_ext}>>), do: {:ok, %#{module_name}.Message.#{message_module_name}{#{unpack_struct_fields},#{unpack_struct_fields_ext}}} """, module: """ defmodule #{module_name}.Message.#{message_module_name} do @enforce_keys [#{enforce_field_names}] defstruct [#{field_names}] @typedoc "#{escape(message.description)}" @type t :: %#{module_name}.Message.#{message_module_name}{#{field_types}} defimpl XMAVLink.Message do def pack(msg, 1), do: {:ok, #{message.id}, #{module_name}.msg_attributes(#{message.id}), <<#{pack_binary_pattern}>>} def pack(msg, 2), do: {:ok, #{message.id}, #{module_name}.msg_attributes(#{message.id}), <<#{pack_binary_pattern},#{pack_binary_pattern_ext}>>} end end """ } else %{ msg_attributes: """ def msg_attributes(#{message.id}), do: {:ok, #{crc_extra}, #{expected_payload_size}, :#{target}} """, unpack: """ def unpack(#{message.id}, _, <<#{unpack_binary_pattern}>>), do: {:ok, %#{module_name}.Message.#{message_module_name}{#{unpack_struct_fields}}} """, module: """ defmodule #{module_name}.Message.#{message_module_name} do @enforce_keys [#{enforce_field_names}] defstruct [#{field_names}] @typedoc "#{escape(message.description)}" @type t :: %#{module_name}.Message.#{message_module_name}{#{field_types}} defimpl XMAVLink.Message do def pack(msg, _), do: {:ok, #{message.id}, #{module_name}.msg_attributes(#{message.id}), <<#{pack_binary_pattern}>>} end end """ } end end end @spec calculate_message_crc_extra(XMAVLink.Parser.message_description()) :: XMAVLink.Types.crc_extra() defp calculate_message_crc_extra(message) do reduce( # Do not include extension fields message.fields |> wire_order |> hd, x25_crc(message.name <> " "), fn field, crc -> case field.ordinality do 1 -> crc |> x25_crc(field.type <> " ") |> x25_crc(field.name <> " ") _ -> crc |> x25_crc(field.type <> " ") |> x25_crc(field.name <> " ") |> x25_crc([field.ordinality]) end end ) |> eight_bit_checksum end # Unpack Message Fields defp unpack_field_code_fragment(%{name: name, ordinality: 1, enum: "", type: "float"}, _) do "unpack_float(#{downcase(name)}_f)" end defp unpack_field_code_fragment(%{name: name, ordinality: 1, enum: "", type: "double"}, _) do "unpack_double(#{downcase(name)}_f)" end defp unpack_field_code_fragment(%{name: name, ordinality: 1, enum: ""}, _) do downcase(name) <> "_f" end defp unpack_field_code_fragment(%{name: name, ordinality: 1, enum: enum, display: :bitmask}, _) when enum != "" do "unpack_bitmask(#{downcase(name)}_f, :#{enum}, &decode/2)" end defp unpack_field_code_fragment(%{name: name, ordinality: 1, enum: enum}, enums_by_name) do case looks_like_a_bitmask?(enums_by_name[enum]) do true -> IO.puts(~s[Warning: assuming #{enum} is a bitmask although display="bitmask" not set]) "unpack_bitmask(#{downcase(name)}_f, :#{enum}, &decode/2)" false -> "decode(#{downcase(name)}_f, :#{enum})" end end defp unpack_field_code_fragment(%{name: name, type: "char"}, _) do ~s[replace_trailing(#{downcase(name)}_f, <<0>>, "")] end defp unpack_field_code_fragment(%{name: name, type: type}, _) do "unpack_array(#{downcase(name)}_f, fn(<>) -> {elem, rest} end)" end # Pack Message Fields defp pack_field_code_fragment(%{name: name, ordinality: 1, enum: "", type: "float"}, _, _) do "XMAVLink.Utils.pack_float(msg.#{downcase(name)})::binary-size(4)" end defp pack_field_code_fragment(%{name: name, ordinality: 1, enum: "", type: "double"}, _, _) do "XMAVLink.Utils.pack_double(msg.#{downcase(name)})::binary-size(8)" end defp pack_field_code_fragment(%{name: name, ordinality: 1, enum: "", type: type}, _, _) do "msg.#{downcase(name)}::#{type_to_binary(type).pattern}" end defp pack_field_code_fragment( %{name: name, ordinality: 1, enum: enum, display: :bitmask, type: type}, _, module_name ) when enum != "" do "#{module_name}.pack_bitmask(msg.#{downcase(name)}, :#{enum}, &#{module_name}.encode/2)::#{type_to_binary(type).pattern}" end defp pack_field_code_fragment( %{name: name, ordinality: 1, enum: enum, type: type}, enums_by_name, module_name ) do case looks_like_a_bitmask?(enums_by_name[enum]) do true -> "#{module_name}.pack_bitmask(msg.#{downcase(name)}, :#{enum}, &#{module_name}.encode/2)::#{type_to_binary(type).pattern}" false -> "#{module_name}.encode(msg.#{downcase(name)}, :#{enum})::#{type_to_binary(type).pattern}" end end defp pack_field_code_fragment(%{name: name, ordinality: ordinality, type: "char"}, _, _) do "XMAVLink.Utils.pack_string(msg.#{downcase(name)}, #{ordinality})::binary-size(#{ordinality})" end defp pack_field_code_fragment(%{name: name, ordinality: ordinality, type: type}, _, _) do "XMAVLink.Utils.pack_array(msg.#{downcase(name)}, #{ordinality}, fn(elem) -> <> end)::binary-size(#{type_to_binary(type).size * ordinality})" end @spec get_unit_code_fragments([XMAVLink.Parser.message_description()]) :: [String.t()] defp get_unit_code_fragments(messages) do reduce( messages, MapSet.new(), fn message, units -> reduce( message.fields, units, fn %{units: next_unit}, units -> cond do next_unit == nil -> units Regex.match?(~r/^[a-zA-Z0-9@_]+$/, Atom.to_string(next_unit)) -> MapSet.put(units, ~s(:#{next_unit})) true -> MapSet.put(units, ~s(:"#{next_unit}")) end end ) end ) |> MapSet.to_list() |> Enum.sort() end @spec module_case(String.t()) :: String.t() defp module_case(name) do name |> split("_") |> map(&capitalize/1) |> join("") end # Some bitmask fields e.g. EkfStatusReport.flags are not marked with display="bitmask". This function # returns true if the enum entry values start with 1, 2, 4 and then continue increasing through powers of 2. defp looks_like_a_bitmask?(%{entries: entries}), do: looks_like_a_bitmask?(entries |> map(& &1.value) |> sort) defp looks_like_a_bitmask?([1, 2, 4 | rest]), do: looks_like_a_bitmask?(rest) defp looks_like_a_bitmask?([8 | rest]), do: looks_like_a_bitmask?(rest |> map(&(&1 >>> 1))) defp looks_like_a_bitmask?([]), do: true defp looks_like_a_bitmask?(_), do: false # Have to deal with some overlap between MAVLink and Elixir types defp field_type(%{type: type, ordinality: ordinality, enum: enum}, module_name) when ordinality > 1, do: "[ #{field_type(%{type: type, ordinality: 1, enum: enum}, module_name)} ]" defp field_type(%{enum: enum, display: :bitmask}, module_name) when enum != "", do: "MapSet.t(#{module_name}.Types.#{enum})" defp field_type(%{enum: enum}, module_name) when enum != "", do: "#{module_name}.Types.#{enum}" defp field_type(%{type: "char"}, _), do: "char" defp field_type(%{type: "float"}, _), do: "Float32" defp field_type(%{type: "double"}, _), do: "Float64" defp field_type(%{type: type}, _), do: "XMAVLink.Types.#{type}" # Map field types to a binary pattern code fragment and a size defp type_to_binary("char"), do: %{pattern: "integer-size(8)", size: 1} defp type_to_binary("uint8_t"), do: %{pattern: "integer-size(8)", size: 1} defp type_to_binary("int8_t"), do: %{pattern: "signed-integer-size(8)", size: 1} defp type_to_binary("uint16_t"), do: %{pattern: "little-integer-size(16)", size: 2} defp type_to_binary("int16_t"), do: %{pattern: "little-signed-integer-size(16)", size: 2} defp type_to_binary("uint32_t"), do: %{pattern: "little-integer-size(32)", size: 4} defp type_to_binary("int32_t"), do: %{pattern: "little-signed-integer-size(32)", size: 4} defp type_to_binary("uint64_t"), do: %{pattern: "little-integer-size(64)", size: 8} defp type_to_binary("int64_t"), do: %{pattern: "little-signed-integer-size(64)", size: 8} # Delegate to (un)pack_float to handle :nan defp type_to_binary("float"), do: %{pattern: "binary-size(4)", size: 4} # " " (un)pack_double defp type_to_binary("double"), do: %{pattern: "binary-size(8)", size: 8} @spec escape(String.t()) :: String.t() defp escape(s) do replace(s, ~s("), ~s(\\")) end end