lib/realflight_integration.ex

defmodule RealflightIntegration do
  alias RealflightIntegration.Utils

  @moduledoc """
  Documentation for `RealflightIntegration`.
  """
  require Logger
  use Bitwise
  use GenServer

  # @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

  @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
  @software_update_actuators :software_update_actuators

  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__)
    ViaUtils.Comms.join_group(__MODULE__, @software_update_actuators, self())

    url = Keyword.fetch!(config, :host_ip) <> ":18083"
    Logger.debug("url: #{url}")

    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 = %{
      url: url,
      bodyaccel_mpss: %{},
      attitude_rad: %{},
      bodyrate_rps: %{},
      position_rrm: %{},
      velocity_mps: %{},
      position_origin_rrm:
        ViaUtils.Location.new_location_input_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
    }

    restore_controller(url)

    inject_controller_interface(url)

    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(), publish_downward_tof_distance_interval_ms, @down_tof_loop)
    ViaUtils.Process.start_loop(self(), config[:sim_loop_interval_ms], @exchange_data_loop)
    {:ok, state}
  end

  @impl GenServer
  def terminate(reason, state) do
    Logger.error("#{__MODULE__} terminated for #{inspect(reason)}")
    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({:update_actuators, output_map}, state) do
  #   servo_out =
  #     if Enum.empty?(output_map) do
  #       state.servo_out
  #     else
  #       output_map =
  #         Enum.reduce(output_map, %{}, fn {actuator_name, {actuator, output}}, acc ->
  #           if actuator.reversed do
  #             Map.put(acc, actuator_name, 1.0 - output)
  #           else
  #             Map.put(acc, actuator_name, output)
  #           end
  #         end)

  #       # Logger.debug("output map: #{inspect(output_map)}")
  #       aileron = Map.get(output_map, :aileron, 0.5)
  #       elevator = Map.get(output_map, :elevator, 0.5)
  #       throttle = Map.get(output_map, :throttle, 0.0)
  #       rudder = Map.get(output_map, :rudder, 0.5)
  #       flaps = Map.get(output_map, :flaps, 0.0)
  #       [aileron, 1 - elevator, throttle, rudder, 0, flaps, 0, 0, 0, 0, 0, 0]
  #     end

  #   # 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(@exchange_data_loop, state) do
    state = exchange_data(state, state.servo_out)
    # state = exchange_data(state, state.rcin)
    {:noreply, state}
  end

  @impl GenServer
  def handle_info({@dt_accel_gyro_loop, dt_s}, state) do
    bodyaccel_mpss = state.bodyaccel_mpss
    bodyrate_rps = state.bodyrate_rps

    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,
        state.dt_accel_gyro_group
      )
    end

    {:noreply, state}
  end

  @impl GenServer
  def handle_info(@gps_pos_vel_loop, state) do
    position_rrm = state.position_rrm
    velocity_mps = state.velocity_mps

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

    {:noreply, state}
  end

  @impl GenServer
  def handle_info(@gps_relhdg_loop, state) do
    attitude_rad = state.attitude_rad

    unless Enum.empty?(attitude_rad) do
      ViaUtils.Simulation.publish_gps_relheading(
        __MODULE__,
        attitude_rad.yaw_rad,
        state.gps_itow_relheading_group
      )
    end

    {:noreply, state}
  end

  @impl GenServer
  def handle_info(@airspeed_loop, state) do
    airspeed_mps = state.airspeed_mps

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

    {:noreply, state}
  end

  @impl GenServer
  def handle_info(@down_tof_loop, state) do
    attitude_rad = state.attitude_rad
    agl_m = state.agl_m

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

    {:noreply, state}
  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")
    post_poison(url, body)
  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>"
    post_poison(url, body)
  end

  @spec exchange_data(map(), list()) :: atom()
  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)
      # Logger.debug("position: #{ViaUtils.Location.to_string(position)}")
      velocity = Utils.extract_velocity(aircraft_state)
      # Logger.debug("velocity: #{ViaUtils.Format.eftb_map(velocity, 2)}")
      attitude = Utils.extract_attitude(aircraft_state)
      # Logger.debug("attitude: #{ViaUtils.Format.eftb_map_deg(attitude, 1)}")
      bodyaccel = Utils.extract_bodyaccel(aircraft_state)
      # Logger.debug("bodyaccel: #{ViaUtils.Format.eftb_map(bodyaccel, 3)}")
      bodyrate = Utils.extract_bodyrate(aircraft_state)
      # Logger.debug(ViaUtils.Format.eftb_map_deg(bodyrate, 2))
      agl = Utils.extract_agl(aircraft_state)
      # Logger.debug("agl: #{agl}")
      airspeed = Utils.extract_airspeed(aircraft_state)
      # Logger.debug("airspeed: #{airspeed}")
      rcin = Utils.extract_rcin(rcin_values)
      servo_out = if state.rc_passthrough, do: rcin, else: state.servo_out
      # Logger.debug("rcin: #{inspect(rcin)}")
      # 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
      }
    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
end