lib/realflight_integration/send_receive.ex

defmodule RealflightIntegration.SendReceive do
  alias RealflightIntegration.Utils

  @moduledoc """
  Documentation for `RealflightIntegration`.
  """
  require Logger
  use Bitwise
  use GenServer
  require ViaUtils.Shared.Groups, as: Groups
  require ViaUtils.Shared.ValueNames, as: SVN
  require ViaUtils.File
  alias ViaUtils.Watchdog

  # @rad2deg 57.295779513
  @default_latitude 41.769201
  @default_longitude -122.506394
  @default_servo [0.5, 0.5, 0, 0.5, 0.5, 0, 0.5, 0, 0.5, 0.5, 0.5, 0.5]
  @rf_stick_mult 1.07

  @start_exchange_data_loop :start_exchange_data_loop
  @exchange_data_loop :exchange_data_loop
  @dt_accel_gyro_loop :dt_accel_gyro_loop
  @gps_pos_vel_loop :gps_pos_vel_loop
  @gps_relhdg_loop :gps_relhdg_loop
  @airspeed_loop :airspeed_loop
  @down_tof_loop :down_tof_loop
  @clear_exchange_callback :clear_exchange_callback

  @url_port 18083

  @ip_filename "realflight.txt"
  def start_link(config) do
    Logger.debug("Start RealflightIntegration GenServer")
    ViaUtils.Process.start_link_redundant(GenServer, __MODULE__, config, __MODULE__)
  end

  @impl GenServer
  def init(config) do
    ViaUtils.Comms.start_operator(__MODULE__)

    realflight_ip_address = Keyword.get(config, :realflight_ip)

    publish_dt_accel_gyro_interval_ms = config[:publish_dt_accel_gyro_interval_ms]
    publish_gps_position_velocity_interval_ms = config[:publish_gps_position_velocity_interval_ms]
    publish_gps_relative_heading_interval_ms = config[:publish_gps_relative_heading_interval_ms]
    publish_airspeed_interval_ms = config[:publish_airspeed_interval_ms]
    publish_downward_tof_distance_interval_ms = config[:publish_downward_tof_distance_interval_ms]

    state = %{
      realflight_ip_address: realflight_ip_address,
      host_ip_address: nil,
      url: nil,
      bodyaccel_mpss: %{},
      attitude_rad: %{},
      bodyrate_rps: %{},
      position_rrm: %{},
      velocity_mps: %{},
      position_origin_rrm: ViaUtils.Location.new_degrees(@default_latitude, @default_longitude),
      agl_m: nil,
      airspeed_mps: nil,
      rcin: @default_servo,
      servo_out: @default_servo,
      rc_passthrough: Keyword.get(config, :rc_passthrough, false),
      # pwm_channels: Keyword.fetch!(config, :pwm_channels),
      # reversed_channels: Keyword.fetch!(config, :reversed_channels),
      dt_accel_gyro_group: config[:dt_accel_gyro_group],
      gps_itow_position_velocity_group: config[:gps_itow_position_velocity_group],
      gps_itow_relheading_group: config[:gps_itow_relheading_group],
      airspeed_group: config[:airspeed_group],
      downward_tof_distance_group: config[:downward_tof_distance_group],
      publish_dt_accel_gyro_interval_ms: publish_dt_accel_gyro_interval_ms,
      publish_gps_position_velocity_interval_ms: publish_gps_position_velocity_interval_ms,
      publish_gps_relative_heading_interval_ms: publish_gps_relative_heading_interval_ms,
      publish_airspeed_interval_ms: publish_airspeed_interval_ms,
      publish_downward_tof_distance_interval_ms: publish_downward_tof_distance_interval_ms,
      exchange_data_loop_interval_ms: config[:sim_loop_interval_ms],
      exchange_data_watchdog: Watchdog.new(@clear_exchange_callback, 10000),
      exchange_data_timer: nil
    }

    ViaUtils.Process.start_loop(
      self(),
      publish_dt_accel_gyro_interval_ms,
      {@dt_accel_gyro_loop, publish_dt_accel_gyro_interval_ms * 1.0e-3}
    )

    ViaUtils.Process.start_loop(
      self(),
      publish_gps_position_velocity_interval_ms,
      @gps_pos_vel_loop
    )

    ViaUtils.Process.start_loop(
      self(),
      publish_gps_relative_heading_interval_ms,
      @gps_relhdg_loop
    )

    ViaUtils.Process.start_loop(
      self(),
      state.publish_downward_tof_distance_interval_ms,
      @down_tof_loop
    )

    ViaUtils.Comms.join_group(__MODULE__, Groups.simulation_update_actuators(), self())
    ViaUtils.Comms.join_group(__MODULE__, Groups.set_realflight_ip_address())
    ViaUtils.Comms.join_group(__MODULE__, Groups.get_realflight_ip_address())
    ViaUtils.Comms.join_group(__MODULE__, Groups.host_ip_address())

    check_and_set_rf_ip_address(realflight_ip_address)
    {:ok, state}
  end

  @impl GenServer
  def terminate(reason, state) do
    Logger.error("#{__MODULE__} terminated for #{inspect(reason)}")
    state
  end

  def initialize_exchange_data(state) do
    Logger.debug("EFI init ex data")

    reset_realflight_interface()

    ViaUtils.Comms.cast_local_msg_to_group(
      __MODULE__,
      {Groups.realflight_ip_address(), state.realflight_ip_address},
      self()
    )

    :erlang.send_after(1000, self(), @start_exchange_data_loop)
    state
  end

  def reset_realflight_interface() do
    Logger.debug("reset realflight interface")
    restore_controller()
    inject_controller_interface()
  end

  def restart_exchange_data_timer(exchange_data_timer, interval_ms) do
    Logger.info("RFI start loops: #{interval_ms}")

    if is_nil(exchange_data_timer) do
      ViaUtils.Process.start_loop(
        self(),
        interval_ms,
        @exchange_data_loop
      )
    else
      exchange_data_timer
    end
  end

  def check_and_set_rf_ip_address(fallback_ip) do
    Logger.debug("RFI checking for rf ip")

    realflight_ip_binary =
      ViaUtils.File.read_file_target(
        @ip_filename,
        ViaUtils.File.default_mount_path(),
        ViaUtils.File.target?()
      )

    Logger.debug("RFI ip = #{realflight_ip_binary}")

    realflight_ip =
      cond do
        !is_nil(realflight_ip_binary) -> realflight_ip_binary |> String.trim_trailing("\n")
        !is_nil(fallback_ip) -> fallback_ip
        true -> raise "No valid Realflight IP address available"
      end

    Logger.debug("RFI Realflight IP found: #{inspect(realflight_ip)}")
    GenServer.cast(__MODULE__, {Groups.set_realflight_ip_address(), realflight_ip})
  end

  @impl GenServer
  def handle_cast({Groups.host_ip_address(), host_ip_address}, state) do
    state =
      if !is_nil(host_ip_address) and !is_nil(state.realflight_ip_address) do
        Logger.debug("RFI Host IP Updated")
        Logger.debug("rfi ip: #{state.realflight_ip_address}")
        initialize_exchange_data(state)
      else
        state
      end

    {:noreply, %{state | host_ip_address: host_ip_address}}
  end

  @impl GenServer
  def handle_cast({Groups.set_realflight_ip_address(), realflight_ip_address}, state) do
    Logger.debug("RFI received RF IP: #{realflight_ip_address}")

    ViaUtils.File.write_file(@ip_filename, "/data/", realflight_ip_address)

    ViaUtils.Comms.cast_local_msg_to_group(
      __MODULE__,
      {Groups.realflight_ip_address(), realflight_ip_address},
      self()
    )

    url = realflight_ip_address <> ":#{@url_port}"
    state = %{state | url: url, realflight_ip_address: realflight_ip_address}

    state =
      if !is_nil(state.host_ip_address) do
        Logger.debug("RFI Host IP Updated")
        Logger.debug("rfi ip: #{state.realflight_ip_address}")
        Logger.debug("host ip: #{state.host_ip_address}")
        Process.sleep(1000)
        initialize_exchange_data(state)
      else
        state
      end

    {:noreply, state}
  end

  @impl GenServer
  def handle_cast({Groups.get_realflight_ip_address(), from}, state) do
    Logger.debug("RF rx get_rf_ip: #{state.realflight_ip_address}")

    GenServer.cast(from, {Groups.realflight_ip_address(), state.realflight_ip_address})

    {:noreply, state}
  end

  @impl GenServer
  def handle_cast({:post, msg, params}, state) do
    Logger.debug("post: #{msg}")

    state =
      case msg do
        :reset ->
          reset_aircraft(state.url)
          state

        :restore ->
          restore_controller(state.url)
          state

        :inject ->
          inject_controller_interface(state.url)
          state

        :exchange ->
          exchange_data(state, params)
      end

    {:noreply, state}
  end

  @impl GenServer
  def handle_cast(
        {Groups.simulation_update_actuators(), actuators_and_outputs, is_override},
        state
      ) do
    # Logger.debug("output map: #{ViaUtils.Format.eftb_map(actuators_and_outputs,3)}")
    [aileron_prev, elevator_prev, throttle_prev, rudder_prev, _, flaps_prev, _, _, _, _, _, _] =
      state.servo_out

    aileron =
      case Map.get(actuators_and_outputs, :aileron_scaled) do
        nil ->
          Logger.error("RFI aileron is nil")
          aileron_prev

        aileron_two_sided ->
          get_one_sided_value(aileron_two_sided)
      end

    elevator =
      case Map.get(actuators_and_outputs, :elevator_scaled) do
        nil ->
          Logger.error("RFI elevator is nil")
          elevator_prev

        elevator_two_sided ->
          get_one_sided_value(elevator_two_sided)
      end

    elevator = if is_override, do: elevator, else: 1 - elevator

    throttle =
      case Map.get(actuators_and_outputs, :throttle_scaled) do
        nil ->
          Logger.error("RFI throttle is nil")
          throttle_prev

        throttle ->
          throttle
      end

    rudder =
      case Map.get(actuators_and_outputs, :rudder_scaled) do
        nil ->
          Logger.error("RFI rudder is nil")
          rudder_prev

        rudder_two_sided ->
          get_one_sided_value(rudder_two_sided)
      end

    flaps =
      case Map.get(actuators_and_outputs, :flaps_scaled) do
        nil ->
          Logger.error("RFI flaps is nil")
          flaps_prev

        flaps ->
          flaps
      end

    servo_out = [aileron, elevator, throttle, rudder, 0, flaps, 0, 0, 0, 0, 0, 0]

    # Logger.debug("servo_out: #{inspect(servo_out)}")
    {:noreply, %{state | servo_out: servo_out}}
  end

  # @impl GenServer
  # def handle_cast({:pwm_input, scaled_values}, state) do
  #   # Logger.debug("pwm ch: #{inspect(pwm_channels)}")
  #   # Logger.info("scaled: #{Common.Utils.eftb_list(scaled_values, 3)}")
  #   cmds_reverse =
  #     Enum.reduce(Enum.with_index(scaled_values), [], fn {ch_value, _index}, acc ->
  #       [ch_value] ++ acc
  #     end)

  #   cmds_reverse =
  #     Enum.reduce(1..(11 - length(scaled_values)), cmds_reverse, fn _x, acc ->
  #       [0] ++ acc
  #     end)

  #   cmds =
  #     Enum.reverse(cmds_reverse)
  #     |> List.insert_at(4, 0)

  #   # Logger.debug(Common.Utils.eftb_list(cmds, 2))
  #   {:noreply, %{state | servo_out: cmds}}
  # end

  def handle_info(@start_exchange_data_loop, state) do
    exchange_data_timer =
      restart_exchange_data_timer(
        state.exchange_data_timer,
        state.exchange_data_loop_interval_ms
      )

    {:noreply, %{state | exchange_data_timer: exchange_data_timer}}
  end

  def handle_info(@exchange_data_loop, state) do
    state = exchange_data(state, state.servo_out)

    rcin =
      Enum.map(state.rcin, fn x ->
        get_two_sided_value(x)
      end)

    ViaUtils.Comms.cast_global_msg_to_group(
      __MODULE__,
      {Groups.command_channels(), rcin},
      self()
    )

    {:noreply, state}
  end

  @impl GenServer
  def handle_info({@dt_accel_gyro_loop, dt_s}, state) do
    %{bodyaccel_mpss: bodyaccel_mpss, bodyrate_rps: bodyrate_rps, dt_accel_gyro_group: group} =
      state

    unless Enum.empty?(bodyaccel_mpss) or Enum.empty?(bodyrate_rps) do
      # Logger.debug("br: #{ViaUtils.Format.eftb_map_deg(bodyrate_rps, 1)}")

      ViaUtils.Simulation.publish_dt_accel_gyro(
        __MODULE__,
        dt_s,
        bodyaccel_mpss,
        bodyrate_rps,
        group
      )
    end

    {:noreply, state}
  end

  @impl GenServer
  def handle_info(@gps_pos_vel_loop, state) do
    %{
      position_rrm: position_rrm,
      velocity_mps: velocity_mps,
      gps_itow_position_velocity_group: group
    } = state

    unless Enum.empty?(position_rrm) or Enum.empty?(velocity_mps) do
      ViaUtils.Simulation.publish_gps_itow_position_velocity(
        __MODULE__,
        position_rrm,
        velocity_mps,
        group
      )
    end

    {:noreply, state}
  end

  @impl GenServer
  def handle_info(@gps_relhdg_loop, state) do
    %{attitude_rad: attitude_rad, gps_itow_relheading_group: group} = state

    unless Enum.empty?(attitude_rad) do
      %{SVN.yaw_rad() => yaw_rad} = attitude_rad

      ViaUtils.Simulation.publish_gps_relheading(
        __MODULE__,
        yaw_rad,
        group
      )
    end

    {:noreply, state}
  end

  @impl GenServer
  def handle_info(@airspeed_loop, state) do
    %{airspeed_mps: airspeed_mps, airspeed_group: group} = state

    unless is_nil(airspeed_mps) do
      ViaUtils.Simulation.publish_airspeed(__MODULE__, airspeed_mps, group)
    end

    {:noreply, state}
  end

  @impl GenServer
  def handle_info(@down_tof_loop, state) do
    %{attitude_rad: attitude_rad, agl_m: agl_m, downward_tof_distance_group: group} = state

    unless Enum.empty?(attitude_rad) or is_nil(agl_m) do
      ViaUtils.Simulation.publish_downward_tof_distance(
        __MODULE__,
        attitude_rad,
        agl_m,
        group
      )
    end

    {:noreply, state}
  end

  @impl GenServer
  def handle_info(@clear_exchange_callback, state) do
    Logger.warn("#{inspect(__MODULE__)} clear is_exchange_current}")
    url = state.url
    restore_controller(url)
    inject_controller_interface(url)

    {:noreply, %{state | exchange_data_watchdog: Watchdog.reset(state.exchange_data_watchdog)}}
  end

  def fix_rx(x) do
    (x - 0.5) * @rf_stick_mult + 0.5
  end

  @spec reset_aircraft(binary()) :: binary()
  def reset_aircraft(url) do
    body = "<?xml version='1.0' encoding='UTF-8'?>
    <soap:Envelope xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/' xmlns:xsd='http://www.w3.org/2001/XMLSchema' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>
    <soap:Body>
    <ResetAircraft><a>1</a><b>2</b></ResetAircraft>
    </soap:Body>
    </soap:Envelope>"
    Logger.debug("body: #{inspect(body)}")
    Logger.debug("reset")
    response = post_poison(url, body)
    Logger.debug("reset response: #{response}")
    # Logger.debug("#{inspect(Soap.Response.parse(response.body))}")
    response
  end

  @spec restore_controller(binary()) :: binary()
  def restore_controller(url) do
    body = "<?xml version='1.0' encoding='UTF-8'?>
    <soap:Envelope xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/' xmlns:xsd='http://www.w3.org/2001/XMLSchema' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>
    <soap:Body>
    <RestoreOriginalControllerDevice><a>1</a><b>2</b></RestoreOriginalControllerDevice>
    </soap:Body>
    </soap:Envelope>"
    Logger.debug("restore controller")
    post_poison(url, body, 1000)
  end

  @spec inject_controller_interface(binary()) :: binary()
  def inject_controller_interface(url) do
    body = "<?xml version='1.0' encoding='UTF-8'?>
    <soap:Envelope xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/' xmlns:xsd='http://www.w3.org/2001/XMLSchema' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>
    <soap:Body>
    <InjectUAVControllerInterface><a>1</a><b>2</b></InjectUAVControllerInterface>
    </soap:Body>
    </soap:Envelope>"
    Logger.debug("inject controller interface")
    post_poison(url, body, 1000)
  end

  @spec exchange_data(map(), list()) :: map()
  def exchange_data(state, servo_output) do
    # start_time = :os.system_time(:microsecond)
    # Logger.debug("start: #{start_time}")
    body_header = "<?xml version='1.0' encoding='UTF-8'?>
    <soap:Envelope xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/' xmlns:xsd='http://www.w3.org/2001/XMLSchema' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>
    <soap:Body>
    <ExchangeData>
    <pControlInputs>
    <m-selectedChannels>4095</m-selectedChannels>
    <m-channelValues-0to1>"
    body_footer = "</m-channelValues-0to1>
                                 </pControlInputs>
                                 </ExchangeData>
                                 </soap:Body>
                                 </soap:Envelope>"

    servo_str =
      Enum.reduce(servo_output, "", fn value, acc ->
        acc <> "<item>#{ViaUtils.Format.eftb(value, 4)}</item>"
      end)

    body = body_header <> servo_str <> body_footer
    # Logger.debug("body: #{inspect(body)}")
    response = post_poison(state.url, body)

    xml_map =
      case SAXMap.from_string(response) do
        {:ok, xml} -> xml
        _other -> %{}
      end

    return_data = get_in(xml_map, return_data_path())
    # Logger.info("#{inspect(return_data)}")
    if is_nil(return_data) do
      state
    else
      aircraft_state = Utils.extract_from_path(return_data, aircraft_state_path())
      rcin_values = Utils.extract_from_path(return_data, rcin_path())
      position = Utils.extract_position(aircraft_state, state.position_origin_rrm)
      velocity = Utils.extract_velocity(aircraft_state)
      attitude = Utils.extract_attitude(aircraft_state)
      bodyaccel = Utils.extract_bodyaccel(aircraft_state)
      bodyrate = Utils.extract_bodyrate(aircraft_state)
      agl = Utils.extract_agl(aircraft_state)
      airspeed = Utils.extract_airspeed(aircraft_state)
      rcin = Utils.extract_rcin(rcin_values)
      servo_out = if state.rc_passthrough, do: rcin, else: state.servo_out

      # Logger.debug("rcin: #{inspect(rcin)}")
      # Logger.debug("position: #{ViaUtils.Location.to_string(position)}")
      # Logger.debug("velocity: #{ViaUtils.Format.eftb_map(velocity, 2)}")
      # Logger.debug("attitude: #{ViaUtils.Format.eftb_map_deg(attitude, 1)}")
      # Logger.debug("bodyaccel: #{ViaUtils.Format.eftb_map(bodyaccel, 3)}")
      # Logger.debug(ViaUtils.Format.eftb_map_deg(bodyrate, 2))
      # Logger.debug("agl: #{ViaUtils.Format.eftb(agl,2)}")
      # Logger.debug("airspeed: #{ViaUtils.Format.eftb(airspeed,2)}")

      # end_time = :os.system_time(:microsecond)
      # Logger.debug("dt: #{ViaUtils.Format.eftb((end_time-start_time)*0.001,1)}")
      %{
        state
        | bodyaccel_mpss: bodyaccel,
          bodyrate_rps: bodyrate,
          attitude_rad: attitude,
          position_rrm: position,
          velocity_mps: velocity,
          agl_m: agl,
          airspeed_mps: airspeed,
          rcin: rcin,
          servo_out: servo_out,
          exchange_data_watchdog: Watchdog.reset(state.exchange_data_watchdog)
      }
    end
  end

  @spec reset_aircraft() :: atom()
  def reset_aircraft() do
    GenServer.cast(__MODULE__, {:post, :reset, nil})
  end

  @spec restore_controller() :: atom()
  def restore_controller() do
    GenServer.cast(__MODULE__, {:post, :restore, nil})
  end

  @spec inject_controller_interface() :: atom()
  def inject_controller_interface() do
    GenServer.cast(__MODULE__, {:post, :inject, nil})
  end

  @spec set_throttle(float()) :: atom()
  def set_throttle(throttle) do
    servos =
      Enum.reduce(0..11, [], fn x, acc ->
        if x == 2, do: [throttle] ++ acc, else: [0.5] ++ acc
      end)
      |> Enum.reverse()

    GenServer.cast(__MODULE__, {:post, :exchange, servos})
  end

  @spec post_poison(binary(), binary(), integer()) :: binary()
  def post_poison(url, body, timeout \\ 10) do
    case HTTPoison.post(url, body, [], timeout: timeout) do
      {:ok, response} ->
        response.body

      {:error, error} ->
        Logger.warn("HTTPoison error: #{inspect(error)}")
        ""
    end
  end

  @spec return_data_path() :: list()
  def return_data_path() do
    ["SOAP-ENV:Envelope", "SOAP-ENV:Body", "ReturnData"]
  end

  @spec aircraft_state_path() :: list()
  def aircraft_state_path() do
    ["m-aircraftState"]
  end

  @spec rcin_path() :: list()
  def rcin_path() do
    ["m-previousInputsState", "m-channelValues-0to1", "item"]
  end

  @spec get_one_sided_value(number()) :: number()
  def get_one_sided_value(two_sided_value) do
    0.5 * two_sided_value + 0.5
  end

  @spec get_two_sided_value(number()) :: number()
  def get_two_sided_value(one_sided_value) do
    2 * one_sided_value - 1
  end
end