lib/mavlink/frame.ex

defmodule XMAVLink.Frame do
  @moduledoc """
  Represent and work with MAVLink v1/2 message frames
  """


  require Logger

  import XMAVLink.Utils, only: [x25_crc: 1, x25_crc: 2]


  defstruct [
    version: nil,              # Which raw attributes are populated?
    payload_length: nil,
    incompatible_flags: 0,      # MAVLink 2 only
    compatible_flags: 0,        # MAVLink 2 only
    sequence_number: nil,
    source_system: nil,
    source_component: nil,
    target_system: 0,          # Default to broadcast assumed elsewhere
    target_component: 0,
    target: nil,
    message_id: nil,
    crc_extra: nil,
    payload: nil,
    checksum: nil,
    signature: nil,             # MAVLink 2 signing only (not implemented)
    mavlink_1_raw: nil,         # Original binary frame
    mavlink_2_raw: nil,
    message: nil
  ]
  @type message :: XMAVLink.Message.t
  @type version :: 1 | 2
  @type t :: %XMAVLink.Frame{
                version: version,
                payload_length: 1..255,
                incompatible_flags: non_neg_integer,
                compatible_flags: non_neg_integer,
                sequence_number: 0..255,
                source_system: 1..255,
                source_component: 1..255,
                target_system: 1..255,
                target_component: 1..255,
                target: :broadcast | :system | :system_component | :component,
                message_id: XMAVLink.Types.message_id,
                crc_extra: XMAVLink.Types.crc_extra,
                payload: binary,
                checksum: pos_integer,
                mavlink_1_raw: binary,
                mavlink_2_raw: binary,
                message: message
             }


  @spec binary_to_frame_and_tail(binary) :: {XMAVLink.Frame.t, binary} | {nil, binary} | :not_a_frame
  def binary_to_frame_and_tail(raw_and_rest=<<0xfe, # MAVLink version 1
      payload_length::unsigned-integer-size(8),
      sequence_number::unsigned-integer-size(8),
      source_system::unsigned-integer-size(8),
      source_component::unsigned-integer-size(8),
      message_id::unsigned-integer-size(8),
      payload::binary-size(payload_length),
      checksum::little-unsigned-integer-size(16),
      rest::binary>>) do
    {
      struct(XMAVLink.Frame, [
        version: 1,
        payload_length: payload_length,
        sequence_number: sequence_number,
        source_system: source_system,
        source_component: source_component,
        message_id: message_id,
        payload: payload,
        checksum: checksum,
        mavlink_1_raw: binary_part(
          raw_and_rest,
          0,
          byte_size(raw_and_rest) - byte_size(rest))
      ]),
      rest
    }
  end

  def binary_to_frame_and_tail(raw_and_rest=<<0xfd, # MAVLink version 2
      payload_length::unsigned-integer-size(8),
      incompatible_flags::unsigned-integer-size(8),
      compatible_flags::unsigned-integer-size(8),
      sequence_number::unsigned-integer-size(8),
      source_system::unsigned-integer-size(8),
      source_component::unsigned-integer-size(8),
      message_id::little-unsigned-integer-size(24),
      payload::binary-size(payload_length),
      checksum::little-unsigned-integer-size(16),
      rest::binary>>) do
    case incompatible_flags do
      0 ->
        # Vanilla MAVLink 2, we can deal with this
        {
          struct(XMAVLink.Frame, [
            version: 2,
            payload_length: payload_length,
            incompatible_flags: 0,
            compatible_flags: compatible_flags,
            sequence_number: sequence_number,
            source_system: source_system,
            source_component: source_component,
            message_id: message_id,
            payload: payload,
            checksum: checksum,
            mavlink_2_raw: binary_part(
              raw_and_rest,
              0,
              byte_size(raw_and_rest) - byte_size(rest))
          ]),
          rest
        }
      _ ->
        # We don't support any incompatible flags at present
        # e.g. signing, so drop the frame
        {nil, rest}
    end
  end

  def binary_to_frame_and_tail(unfinished_mavlink_1_frame=<<0xfe, _::binary>>), do: {nil, unfinished_mavlink_1_frame}
  def binary_to_frame_and_tail(unfinished_mavlink_2_frame=<<0xfd, _::binary>>), do: {nil, unfinished_mavlink_2_frame}
  def binary_to_frame_and_tail(<<_, rest::binary>>), do: binary_to_frame_and_tail(rest)
  def binary_to_frame_and_tail(<<>>), do: :not_a_frame


  @spec validate_and_unpack(XMAVLink.Frame.t, module) :: {:ok, XMAVLink.Frame.t} | :failed_to_unpack | :checksum_invalid | :unknown_message
  def validate_and_unpack(frame=%XMAVLink.Frame{message_id: message_id, version: version, payload: payload}, dialect) do
    case apply(dialect, :msg_attributes, [message_id]) do
      {:ok, crc_extra, expected_length, target} ->
        if frame.checksum == (:binary.bin_to_list(
                                %{1 => frame.mavlink_1_raw, 2 => frame.mavlink_2_raw}[frame.version],
                                {1, frame.payload_length + %{1 => 5, 2 => 9}[frame.version]})
                              |> x25_crc()
                              |> x25_crc([crc_extra])) do
          payload_truncated_length = 8 * (expected_length - frame.payload_length)  # Only used to undo MAVLink 2 payload truncation
          try do  # Too many ways for unpack to fail with dodgy messages...
            case apply(dialect, :unpack, [
              message_id,
              version,
              payload <> (if payload_truncated_length > 0 and version > 1, do: <<0::size(payload_truncated_length)>>, else: <<>>)]) do
              {:ok, message} ->
                case target do
                  :broadcast ->
                    {:ok, struct(frame, [
                      message: message,
                      target_system: 0,
                      target_component: 0,
                      target: target,
                      crc_extra: crc_extra
                    ])}
                  :system ->
                    {:ok, struct(frame, [
                      message: message,
                      target_system: message.target_system,
                      target_component: 0,
                      target: target,
                      crc_extra: crc_extra
                    ])}
                  :system_component ->
                    {:ok, struct(frame, [
                      message: message,
                      target_system: message.target_system,
                      target_component: message.target_component,
                      target: target,
                      crc_extra: crc_extra
                    ])}
                  :component ->
                    {:ok, struct(frame, [
                      message: message,
                      target_system: 0,
                      target_component: message.target_component,
                      target: target,
                      crc_extra: crc_extra
                    ])}
                end
              _ ->
                :failed_to_unpack
            end
          rescue
            _ ->
              :ok = Logger.debug("validate_and_unpack: Failed to unpack #{inspect(frame)}, couldn't match payload")
              :failed_to_unpack
          end
        else
          :ok = Logger.debug("validate_and_unpack: Checksum invalid #{inspect(frame)}")
          :checksum_invalid
        end
      _ ->
        :ok = Logger.debug("validate_and_unpack: Unknown message #{inspect(frame)}")
        :unknown_message
    end
  end


  # Pack message frame
  def pack_frame(frame=%XMAVLink.Frame{version: 1}) do
    payload_length = byte_size(frame.payload)
    mavlink_1_frame = <<payload_length::unsigned-integer-size(8),
              frame.sequence_number::unsigned-integer-size(8),
              frame.source_system::unsigned-integer-size(8),
              frame.source_component::unsigned-integer-size(8),
              frame.message_id::little-unsigned-integer-size(8),
              frame.payload::binary()>>

    frame |> struct([
      mavlink_1_raw: <<0xfe>> <> mavlink_1_frame <> checksum(mavlink_1_frame, frame.crc_extra)
    ])
  end

  def pack_frame(frame=%XMAVLink.Frame{version: 2}) do
    {truncated_length, truncated_payload} = truncate_payload(frame.payload)
    mavlink_2_frame = <<truncated_length::unsigned-integer-size(8),
              0::unsigned-integer-size(8),  # Incompatible flags
              0::unsigned-integer-size(8),  # Compatible flags
              frame.sequence_number::unsigned-integer-size(8),
              frame.source_system::unsigned-integer-size(8),
              frame.source_component::unsigned-integer-size(8),
              frame.message_id::little-unsigned-integer-size(24),
              truncated_payload::binary()>>
    struct(frame,[
      mavlink_2_raw: <<0xfd>> <> mavlink_2_frame <> checksum(mavlink_2_frame, frame.crc_extra)
    ])
  end


  # MAVLink 2 truncate trailing 0s in payload
  defp truncate_payload(payload) do
    truncated_payload = String.replace_trailing(payload, <<0>>, "")
    if byte_size(truncated_payload) == 0 do
      {1, <<0>>}  # First byte of payload never truncated
    else
      {byte_size(truncated_payload), truncated_payload}
    end
  end


  # Calculate checksum
  defp checksum(frame, crc_extra) do
    cs = x25_crc(frame <> <<crc_extra::unsigned-integer-size(8)>>)
    <<cs::little-unsigned-integer-size(16)>>
  end

end