lib/mavlink/serial_connection.ex

defmodule XMAVLink.SerialConnection do
  @moduledoc """
  XMAVLink.Router delegate for Serial connections
  """

  @smallest_mavlink_message 8

  require Logger

  alias XMAVLink.Frame
  alias Circuits.UART

  import XMAVLink.Frame, only: [binary_to_frame_and_tail: 1, validate_and_unpack: 2]

  defstruct port: nil,
            baud: nil,
            uart: nil,
            buffer: <<>>

  @type t :: %XMAVLink.SerialConnection{
          port: binary,
          baud: non_neg_integer,
          uart: pid,
          buffer: binary
        }

  def handle_info(
        {:circuits_uart, port, raw},
        receiving_connection = %XMAVLink.SerialConnection{buffer: buffer},
        dialect
      ) do
    case binary_to_frame_and_tail(buffer <> raw) do
      :not_a_frame ->
        # Noise or malformed frame
        if byte_size(buffer) + byte_size(raw) > 0 do
          :ok =
            Logger.debug("SerialConnection.handle_info: Not a frame: #{inspect(buffer <> raw)}")
        end

        {:error, :not_a_frame, port, struct(receiving_connection, buffer: <<>>)}

      {nil, rest} ->
        {:error, :incomplete_frame, port, struct(receiving_connection, buffer: rest)}

      {received_frame, rest} ->
        # Rest could include a complete message, return later to try emptying the buffer
        if byte_size(rest) >= @smallest_mavlink_message,
          do: send(self(), {:circuits_uart, port, <<>>})

        case validate_and_unpack(received_frame, dialect) do
          {:ok, valid_frame} ->
            {:ok, port, struct(receiving_connection, buffer: rest), valid_frame}

          :unknown_message ->
            # We re-broadcast valid frames with unknown messages
            :ok =
              Logger.debug("rebroadcasting unknown message with id #{received_frame.message_id}}")

            {:ok, port, struct(receiving_connection, buffer: rest),
             struct(received_frame, target: :broadcast)}

          reason ->
            :ok =
              Logger.debug(
                "SerialConnection.handle_info: frame received failed: #{Atom.to_string(reason)}"
              )

            {:error, reason, port, struct(receiving_connection, buffer: rest)}
        end
    end
  end

  def connect(["serial", port, baud, uart], controlling_process) do
    if Map.has_key?(UART.enumerate(), port) do
      case UART.open(uart, port, speed: baud, active: true) do
        :ok ->
          :ok = Logger.info("Opened serial port #{port} at #{baud} baud")

          send(
            controlling_process,
            {
              :add_connection,
              port,
              struct(
                XMAVLink.SerialConnection,
                port: port,
                baud: baud,
                uart: uart
              )
            }
          )

          UART.controlling_process(uart, controlling_process)

        {:error, _} ->
          :ok = Logger.warning("Could not open serial port #{port}. Retrying in 1 second")
          :timer.sleep(1000)
          connect(["serial", port, baud, uart], controlling_process)
      end
    else
      :ok = Logger.warning("Serial port #{port} not attached. Retrying in 1 second")
      :timer.sleep(1000)
      connect(["serial", port, baud, uart], controlling_process)
    end
  end

  def forward(
        %XMAVLink.SerialConnection{uart: uart},
        %Frame{version: 1, mavlink_1_raw: packet}
      ) do
    UART.write(uart, packet)
  end

  def forward(
        %XMAVLink.SerialConnection{uart: uart},
        %Frame{version: 2, mavlink_2_raw: packet}
      ) do
    UART.write(uart, packet)
  end
end