lib/mavlink/tcp_out_connection.ex

defmodule XMAVLink.TCPOutConnection do
  @moduledoc """
  MAVLink.Router delegate for TCP connections
  Typically used to connect to SITL on port 5760
  """

  @smallest_mavlink_message 8

  require Logger
  alias XMAVLink.Frame

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

  defstruct socket: nil, address: nil, port: nil, buffer: <<>>

  @type t :: %XMAVLink.TCPOutConnection{
          socket: pid,
          address: XMAVLink.Types.net_address(),
          port: XMAVLink.Types.net_port(),
          buffer: binary
        }

  def handle_info(
        {:tcp, socket, raw},
        receiving_connection = %XMAVLink.TCPOutConnection{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("TCPOutConnection.handle_info: Not a frame #{inspect(buffer <> raw)}")
        end

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

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

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

        case validate_and_unpack(received_frame, dialect) do
          {:ok, valid_frame} ->
            {:ok, socket, 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, socket, struct(receiving_connection, buffer: rest),
             struct(received_frame, target: :broadcast)}

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

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

  def connect(["tcpout", address, port], controlling_process) do
    case :gen_tcp.connect(address, port, [:binary, active: true]) do
      {:ok, socket} ->
        :ok = Logger.debug("Opened tcpout:#{Enum.join(Tuple.to_list(address), ".")}:#{port}")

        send(
          controlling_process,
          {
            :add_connection,
            socket,
            struct(
              XMAVLink.TCPOutConnection,
              socket: socket,
              address: address,
              port: port
            )
          }
        )

        :gen_tcp.controlling_process(socket, controlling_process)

      other ->
        :ok =
          Logger.debug(
            "Could not open tcpout:#{Enum.join(Tuple.to_list(address), ".")}:#{port}: #{inspect(other)}. Retrying in 1 second"
          )

        :timer.sleep(1000)
        connect(["tcpout", address, port], controlling_process)
    end
  end

  def forward(
        %XMAVLink.TCPOutConnection{socket: socket},
        %Frame{version: 1, mavlink_1_raw: packet}
      ) do
    :gen_udp.send(socket, packet)
  end

  def forward(
        %XMAVLink.TCPOutConnection{socket: socket},
        %Frame{version: 2, mavlink_2_raw: packet}
      ) do
    :gen_udp.send(socket, packet)
  end
end