make sure estops reply to current if it exists

defmodule Farmbot.Firmware do
  @moduledoc "Allows communication with the firmware."

  use GenStage
  use Farmbot.Logger
  alias Farmbot.Firmware.Vec3
  import Farmbot.System.ConfigStorage, only: [update_config_value: 4, get_config_value: 3]

  # If any command takes longer than this, exit.
  @call_timeout 500_000

  @new_params [
    :movement_step_per_mm_x, :movement_step_per_mm_y, :movement_step_per_mm_z,
    :movement_home_spd_x, :movement_home_spd_y, :movement_home_spd_z
  ]

  @doc "Move the bot to a position."
  def move_absolute(%Vec3{} = vec3, x_spd, y_spd, z_spd) do
    call = {:move_absolute, [vec3, x_spd, y_spd, z_spd]}
    GenStage.call(__MODULE__, call, @call_timeout)
  end

  @doc "Calibrate an axis."
  def calibrate(axis) do
    GenStage.call(__MODULE__, {:calibrate, [axis]}, @call_timeout)
  end

  @doc "Find home on an axis."
  def find_home(axis) do
    GenStage.call(__MODULE__, {:find_home, [axis]}, @call_timeout)
  end

  @doc "Home every axis."
  def home_all() do
    GenStage.call(__MODULE__, {:home_all, []}, @call_timeout)
  end

  @doc "Home an axis."
  def home(axis) do
    GenStage.call(__MODULE__, {:home, [axis]}, @call_timeout)
  end

  @doc "Manually set an axis's current position to zero."
  def zero(axis) do
    GenStage.call(__MODULE__, {:zero, [axis]}, @call_timeout)
  end

  @doc """
  Update a paramater.
  For a list of paramaters see `Farmbot.Firmware.Gcode.Param`
  """
  def update_param(param, val) do
    GenStage.call(__MODULE__, {:update_param, [param, val]}, @call_timeout)
  end

  @doc false
  def read_all_params do
    GenStage.call(__MODULE__, {:read_all_params, []}, @call_timeout)
  end

  @doc """
  Read a paramater.
  For a list of paramaters see `Farmbot.Firmware.Gcode.Param`
  """
  def read_param(param) do
    GenStage.call(__MODULE__, {:read_param, [param]}, @call_timeout)
  end

  @doc "Emergency lock Farmbot."
  def emergency_lock() do
    GenStage.call(__MODULE__, {:emergency_lock, []}, @call_timeout)
  end

  @doc "Unlock Farmbot from Emergency state."
  def emergency_unlock() do
    GenStage.call(__MODULE__, {:emergency_unlock, []}, @call_timeout)
  end

  @doc "Set a pin mode (:input | :output)"
  def set_pin_mode(pin, mode) do
    GenStage.call(__MODULE__, {:set_pin_mode, [pin, mode]}, @call_timeout)
  end

  @doc "Read a pin."
  def read_pin(pin, mode) do
    GenStage.call(__MODULE__, {:read_pin, [pin, mode]}, @call_timeout)
  end

  @doc "Write a pin."
  def write_pin(pin, mode, value) do
    GenStage.call(__MODULE__, {:write_pin, [pin, mode, value]}, @call_timeout)
  end

  @doc "Request version."
  def request_software_version do
    GenStage.call(__MODULE__, {:request_software_version, []}, @call_timeout)
  end

  @doc "Set angle of a servo pin."
  def set_servo_angle(pin, value) do
    GenStage.call(__MODULE__, {:set_servo_angle, [pin, value]}, @call_timeout)
  end

  @doc "Start the firmware services."
  def start_link do
    GenStage.start_link(__MODULE__, [], name: __MODULE__)
  end

  ## GenStage

  defmodule Current do
    @moduledoc false
    defstruct [
      fun: nil,
      args: nil,
      from: nil
    ]
  end

  defmodule State do
    @moduledoc false
    defstruct handler: nil, handler_mod: nil,
      idle: false,
      timer: nil,
      pins: %{},
      params: %{},
      initialized: false,
      initializing: false,
      current: nil,
      timeout_ms: 150_000,
      queue: :queue.new()
  end

  def init([]) do
    handler_mod =
      Application.get_env(:farmbot, :behaviour)[:firmware_handler] || raise("No fw handler.")

    case handler_mod.start_link() do
      {:ok, handler} ->
        Process.flag(:trap_exit, true)
        {
          :producer_consumer,
          %State{handler: handler, handler_mod: handler_mod},
          subscribe_to: [handler], dispatcher: GenStage.BroadcastDispatcher
        }
      {:stop, err, state} -> {:stop, err, state}
    end

  end

  def terminate(reason, state) do
    unless :queue.is_empty(state.queue) do
      list = :queue.to_list(state.queue)
      for cmd <- list do
        :ok = do_reply(%{state | current: cmd}, {:error, reason})
      end
    end
  end

  def handle_info({:EXIT, _pid, :normal}, state) do
    {:stop, :normal, state}
  end

  def handle_info({:EXIT, _, reason}, state) do
    Logger.error 1, "Firmware handler: #{state.handler_mod} died: #{inspect reason}"
    {:ok, handler} = state.handler_mod.start_link()
    new_state = %{state | handler: handler}
    {:noreply, [{:informational_settings, %{busy: false}}], %{new_state | initialized: false, idle: false}}
  end

  def handle_info(:timeout, state) do
    case state.current do
      nil -> {:noreply, [], %{state | timer: nil}}
      %Current{fun: fun, args: args, from: _from} = current ->
        Logger.warn 1, "Got Firmware timeout. Retrying #{fun}(#{inspect args}) "
        case apply(state.handler_mod, fun, [state.handler | args]) do
          :ok ->
            timer = Process.send_after(self(), :timeout, state.timeout_ms)
            {:noreply, [], %{state | current: current, timer: timer}}
          {:error, _} = res ->
            do_reply(state, res)
            {:noreply, [], %{state | current: nil, queue: :queue.new()}}
        end
        {:noreply, [], %{state | timer: nil}}
    end
  end

  def handle_call({:fetch_param_hack_delete_me, param}, _, state) do
    {:reply, Map.get(state.params, param), [], state}
  end

  def handle_call({fun, _}, _from, state = %{initialized: false})
  when fun not in  [:read_param, :read_all_params, :update_param, :emergency_unlock, :emergency_lock] do
    {:reply, {:error, :uninitialized}, [], state}
  end

  def handle_call({{:update_param, [param, 0]}}, from, state)
  when param in @new_params do
    Logger.warn 1, "#{param} reported zero when it shouldn't have!"
    defaults = %{
      movement_step_per_mm_x: 5,
      movement_step_per_mm_y: 5,
      movement_step_per_mm_z: 25,
      movement_home_spd_x: 50,
      movement_home_spd_y: 50,
      movement_home_spd_z: 50
    }
    hopefully_still_default_value = get_config_value(:float, "hardware_params", :"#{param}") || defaults[param]
    handle_call({{:update_param, [param, hopefully_still_default_value]}}, from, state)
  end

  def handle_call({fun, args}, from, state) do
    next_current = struct(Current, from: from, fun: fun, args: args)
    current_current = state.current
    cond do
      fun == :emergency_lock ->
        if current_current do
          do_reply(state, {:error, :emergency_lock})
        end
        do_begin_cmd(next_current, state, [])
      match?(%Current{}, current_current) ->
        do_queue_cmd(next_current, state)
      is_nil(current_current) ->
        do_begin_cmd(next_current, state, [])
    end
  end

  defp do_begin_cmd(%Current{fun: fun, args: args, from: _from} = current, state, dispatch) do
    # Logger.busy 3, "FW Starting: #{fun}: #{inspect from}"
    case apply(state.handler_mod, fun, [state.handler | args]) do
      :ok ->
        if fun == :emergency_unlock, do: Farmbot.System.GPIO.Leds.led_status_ok()
        timer = Process.send_after(self(), :timeout, state.timeout_ms)
        {:noreply, dispatch, %{state | current: current, timer: timer}}
      {:error, _} = res ->
        do_reply(%{state | current: current}, res)
        {:noreply, dispatch, %{state | current: nil}}
    end
  end

  defp do_queue_cmd(%Current{fun: _fun, args: _args, from: _from} = current, state) do
    # Logger.busy 3, "FW Queuing: #{fun}: #{inspect from}"
    new_q = :queue.in(current, state.queue)
    {:noreply, [], %{state | queue: new_q}}
  end

  def handle_events(gcodes, _from, state) do
    {diffs, state} = handle_gcodes(gcodes, state)
    # if after handling the current buffer of gcodes,
    # Try to start the next command in the queue if it exists.
    if List.last(gcodes) == :idle && state.current == nil do
      case :queue.out(state.queue) do
        {{:value, next_current}, new_queue} ->
          do_begin_cmd(next_current, %{state | queue: new_queue, current: next_current}, diffs)
        {:empty, queue} -> # nothing to do if the queue is empty.
          {:noreply, diffs, %{state | queue: queue}}
      end
    else
      {:noreply, diffs, state}
    end
  end

  defp handle_gcodes(codes, state, acc \\ [])

  defp handle_gcodes([], state, acc), do: {Enum.reverse(acc), state}

  defp handle_gcodes([code | rest], state, acc) do
    case handle_gcode(code, state) do
      {nil, new_state} -> handle_gcodes(rest, new_state, acc)
      {key, diff, new_state} -> handle_gcodes(rest, new_state, [{key, diff} | acc])
    end
  end

  defp handle_gcode({:debug_message, message}, state) do
    if Farmbot.System.ConfigStorage.get_config_value(:bool, "settings", "arduino_debug_messages") do
      Logger.debug 3, "Arduino debug message: #{message}"
    end
    {nil, state}
  end

  defp handle_gcode(code, state) when code in [:error, :invalid_command] do
    Logger.warn 1, "Got error gcode (#{code})!"
    maybe_cancel_timer(state.timer)
    if state.current do
      formatted_args = Enum.map(state.current.args, fn(arg) ->
        cond do
          is_atom(arg) -> to_string(arg)
          is_binary(arg) -> to_string(arg)
          true -> inspect(arg)
        end
      end)
      Logger.error 1, "Failed to execute #{state.current.fun} #{inspect formatted_args}"
      do_reply(state, {:error, :firmware_error})
      {nil, %{state | current: nil}}
    else
      {nil, state}
    end
  end

  defp handle_gcode({:report_current_position, x, y, z}, state) do
    {:location_data, %{position: %{x: round(x), y: round(y), z: round(z)}}, state}
  end

  defp handle_gcode({:report_encoder_position_scaled, x, y, z}, state) do
    {:location_data, %{scaled_encoders: %{x: x, y: y, z: z}}, state}
  end

  defp handle_gcode({:report_encoder_position_raw, x, y, z}, state) do
    {:location_data, %{raw_encoders: %{x: x, y: y, z: z}}, state}
  end

  defp handle_gcode({:report_end_stops, xa, xb, ya, yb, za, zb}, state) do
    diff = %{end_stops: %{xa: xa, xb: xb, ya: ya, yb: yb, za: za, zb: zb}}
    {:location_data, diff, state}
    {nil, state}
  end

  defp handle_gcode({:report_pin_mode, pin, mode_atom}, state) do
    # Logger.debug 3, "Got pin mode report: #{pin}: #{mode_atom}"
    mode = if(mode_atom == :digital, do: 0, else: 1)
    case state.pins[pin] do
      %{mode: _, value: _} = pin_map ->
        {:pins, %{pin => %{pin_map | mode: mode}}, %{state | pins: %{state.pins | pin => %{pin_map | mode: mode}}}}
      nil ->
        {:pins, %{pin => %{mode: mode, value: -1}}, %{state | pins: Map.put(state.pins, pin, %{mode: mode, value: -1})}}
    end
  end

  defp handle_gcode({:report_pin_value, pin, value}, state) do
    # Logger.debug 3, "Got pin value report: #{pin}: #{value} old: #{inspect state.pins[pin]}"
    case state.pins[pin] do
      %{mode: _, value: _} = pin_map ->
        {:pins, %{pin => %{pin_map | value: value}}, %{state | pins: %{state.pins | pin => %{pin_map | value: value}}}}
      nil ->
        {:pins, %{pin => %{mode: nil, value: value}}, %{state | pins: Map.put(state.pins, pin, %{mode: nil, value: value})}}
    end
  end

  defp handle_gcode({:report_parameter_value, param, 0}, state)
  when param in @new_params do
    Logger.warn 1, "one of #{inspect @new_params} (#{param}) was reported as zero. Not saving."
    {nil, state}
  end

  defp handle_gcode({:report_parameter_value, param, value}, state) when (value == -1) do
    Farmbot.System.ConfigStorage.update_config_value(:float, "hardware_params", to_string(param), nil)
    {:mcu_params, %{param => nil}, %{state | params: Map.put(state.params, param, value)}}
  end

  defp handle_gcode({:report_parameter_value, param, value}, state) when is_number(value) do
    Farmbot.System.ConfigStorage.update_config_value(:float, "hardware_params", to_string(param), value / 1)
    {:mcu_params, %{param => value}, %{state | params: Map.put(state.params, param, value)}}
  end

  defp handle_gcode({:report_parameter_value, param, nil}, state) do
    Farmbot.System.ConfigStorage.update_config_value(:float, "hardware_params", to_string(param), nil)
    {:mcu_params, %{param => nil}, %{state | params: Map.put(state.params, param, nil)}}
  end

  defp handle_gcode(:idle, %{initialized: false, initializing: false} = state) do
    Logger.busy 1, "Initializing Firmware."
    old = Farmbot.System.ConfigStorage.get_config_as_map()["hardware_params"]
    case old["param_version"] do
      nil ->
        Logger.debug 3, "Setting up fresh params."
        spawn __MODULE__, :do_read_params_and_report_position, [%{}]
      _   ->
        Logger.debug 3, "Setting up old params."
        spawn __MODULE__, :do_read_params_and_report_position, [Map.delete(old, "param_version")]
    end
    {nil, %{state | initializing: true}}
  end

  defp handle_gcode(:idle, %{initialized: false, initializing: true} = state) do
    {nil, state}
  end

  defp handle_gcode(:idle, state) do
    maybe_cancel_timer(state.timer)
    Farmbot.BotState.set_busy(false)
    if state.current do
      # This might be a bug in the FW
      if state.current.fun in [:home, :home_all] do
        Logger.warn 1, "Got idle during home. Ignoring. This might be bad."
        timer = Process.send_after(self(), :timeout, state.timeout_ms)
        {nil, %{state | timer: timer}}
      else
        Logger.warn 1, "Got idle while executing a command."
        do_reply(state, {:error, :timeout})
        {:informational_settings, %{busy: false, locked: false}, %{state | current: nil, idle: true}}
      end
    else
      {:informational_settings, %{busy: false, locked: false}, %{state | idle: true}}
    end
  end

  defp handle_gcode(:report_params_complete, state) do
    Logger.success 1, "Firmware initialized."
    {nil, %{state | initializing: false, initialized: true}}
  end

  defp handle_gcode({:report_software_version, version}, state) do
    case String.last(version) do
      "F" ->
        Farmbot.System.ConfigStorage.update_config_value(:string, "settings", "firmware_hardware", "farmduino")
      "R" ->
        Farmbot.System.ConfigStorage.update_config_value(:string, "settings", "firmware_hardware", "arduino")
      _ -> :ok
    end
    {:informational_settings, %{firmware_version: version}, state}
  end

  defp handle_gcode(:report_axis_home_complete_x, state) do
    Logger.success 2, "X Axis homing complete."
    {nil, state}
  end

  defp handle_gcode(:report_axis_home_complete_y, state) do
    Logger.success 2, "Y Axis homing complete."
    {nil, state}
  end

  defp handle_gcode(:report_axis_home_complete_z, state) do
    Logger.success 2, "Z Axis homing complete."
    {nil, state}
  end

  defp handle_gcode(:busy, state) do
    Farmbot.BotState.set_busy(true)
    if state.timer do
      Process.cancel_timer(state.timer)
    end
    timer = Process.send_after(self(), :timeout, state.timeout_ms)
    {:informational_settings, %{busy: true}, %{state | idle: false, timer: timer}}
  end

  defp handle_gcode(:done, state) do
    maybe_cancel_timer(state.timer)
    if state.current do
      do_reply(state, :ok)
      {nil, %{state | current: nil}}
    else
      {nil, state}
    end
  end

  defp handle_gcode(:report_emergency_lock, state) do
    Farmbot.System.GPIO.Leds.led_status_err
    if state.current do
      do_reply(state, {:error, :emergency_lock})
      {:informational_settings, %{locked: true}, %{state | current: nil}}
    else
      {:informational_settings, %{locked: true}, state}
    end
  end

  defp handle_gcode({:report_calibration, axis, status}, state) do
    maybe_cancel_timer(state.timer)
    Logger.busy 1, "Axis #{axis} calibration: #{status}"
    {nil, state}
  end

  defp handle_gcode({:report_axis_calibration, param, val}, state) do
    # have to spawn this fun otherwise we have to functions waiting on eachother.
    # FIXME(Connor) - The Firmware is currently reporting axis calibration values
    #   in mm, not steps, so multiplying by 5 puts it back to steps. :/
    # spawn __MODULE__, :report_calibration_callback, [5, param, val * 5]
    spawn __MODULE__, :report_calibration_callback, [5, param, val]
    {nil, state}
  end

  defp handle_gcode(:noop, state) do
    {nil, state}
  end

  defp handle_gcode(:received, state) do
    {nil, state}
  end

  defp handle_gcode({:echo, _code}, state) do
    {nil, state}
  end

  defp handle_gcode(code, state) do
    Logger.warn(3, "unhandled code: #{inspect(code)}")
    {nil, state}
  end

  defp maybe_cancel_timer(nil), do: :ok
  defp maybe_cancel_timer(timer) do
    if Process.read_timer(timer) do
      Process.cancel_timer(timer)
    end
  end

  @doc false
  def do_read_params_and_report_position(old) when is_map(old) do
    if get_config_value(:bool, "settings", "fw_upgrade_migration") do
      Logger.busy(1, "Migrating old configuration data from firmware.")
      migration_hack = fn(param_atom) ->
        read_param(param_atom)
        val = GenServer.call(__MODULE__, {:fetch_param_hack_delete_me, param_atom})
        modified = cond do
          is_nil(val) -> 5556
          val == 56 -> 5556
          val == 5556 -> 5556
          is_number(val) -> val * 100
          true -> 5556
        end
        Logger.info 2, "Update param: #{param_atom}: #{val} => #{inspect modified}"
        update_param(param_atom, modified)
      end
      for param <- [:encoder_scaling_x, :encoder_scaling_y, :encoder_scaling_z] do
        Logger.busy 3, "Migrating: #{inspect param}"
        migration_hack.(param)
        Logger.success 3, "Done Migrating: #{inspect param}"
      end
      update_config_value(:bool, "settings", "fw_upgrade_migration", false)
      Logger.success(1, "Finished migrating old configuration data.")
    end

    for {key, float_val} <- Map.drop(old, ["encoder_scaling_x", "encoder_scaling_y", "encoder_scaling_z"]) do
      cond do
        (float_val == -1) -> :ok
        is_nil(float_val) -> :ok
        is_number(float_val) ->
          val = round(float_val)
          update_param(:"#{key}", val)
      end
    end
    read_all_params()
    request_software_version()
  end

  @doc false
  def report_calibration_callback(tries, param, value)

  def report_calibration_callback(0, _param, _value) do
    :ok
  end

  def report_calibration_callback(tries, param, val) do
    case Farmbot.Firmware.update_param(param, val) do
      :ok ->
        case Farmbot.System.ConfigStorage.get_config_value(:float, "hardware_params", to_string(param)) do
          ^val ->
            Logger.success 1, "Calibrated #{param}: #{val}"
            :ok
          _ -> report_calibration_callback(tries - 1, param, val)
        end
      {:error, reason} ->
        Logger.error 1, "Failed to set #{param}: #{val} (#{inspect reason})"
        report_calibration_callback(tries - 1, param, val)
    end
  end

  defp do_reply(state, reply) do
    case state.current do
      %Current{fun: :emergency_lock, from: from} ->
        :ok = GenServer.reply from, {:error, :emergency_lock}
      %Current{fun: _fun, from: from} ->
        # Logger.success 3, "FW Replying: #{fun}: #{inspect from}"
        :ok = GenServer.reply from, reply
      nil ->
        Logger.error 1, "FW Nothing to send reply: #{inspect reply} to!."
        :error
    end
  end
end

defmodule Farmbot.Firmware do
  @moduledoc "Allows communication with the firmware."

  use GenStage
  use Farmbot.Logger
  alias Farmbot.Firmware.Vec3
  import Farmbot.System.ConfigStorage, only: [update_config_value: 4, get_config_value: 3]

  # If any command takes longer than this, exit.
  @call_timeout 500_000

  @new_params [
    :movement_step_per_mm_x, :movement_step_per_mm_y, :movement_step_per_mm_z,
    :movement_home_spd_x, :movement_home_spd_y, :movement_home_spd_z
  ]

  @doc "Move the bot to a position."
  def move_absolute(%Vec3{} = vec3, x_spd, y_spd, z_spd) do
    call = {:move_absolute, [vec3, x_spd, y_spd, z_spd]}
    GenStage.call(__MODULE__, call, @call_timeout)
  end

  @doc "Calibrate an axis."
  def calibrate(axis) do
    GenStage.call(__MODULE__, {:calibrate, [axis]}, @call_timeout)
  end

  @doc "Find home on an axis."
  def find_home(axis) do
    GenStage.call(__MODULE__, {:find_home, [axis]}, @call_timeout)
  end

  @doc "Home every axis."
  def home_all() do
    GenStage.call(__MODULE__, {:home_all, []}, @call_timeout)
  end

  @doc "Home an axis."
  def home(axis) do
    GenStage.call(__MODULE__, {:home, [axis]}, @call_timeout)
  end

  @doc "Manually set an axis's current position to zero."
  def zero(axis) do
    GenStage.call(__MODULE__, {:zero, [axis]}, @call_timeout)
  end

  @doc """
  Update a paramater.
  For a list of paramaters see `Farmbot.Firmware.Gcode.Param`
  """
  def update_param(param, val) do
    GenStage.call(__MODULE__, {:update_param, [param, val]}, @call_timeout)
  end

  @doc false
  def read_all_params do
    GenStage.call(__MODULE__, {:read_all_params, []}, @call_timeout)
  end

  @doc """
  Read a paramater.
  For a list of paramaters see `Farmbot.Firmware.Gcode.Param`
  """
  def read_param(param) do
    GenStage.call(__MODULE__, {:read_param, [param]}, @call_timeout)
  end

  @doc "Emergency lock Farmbot."
  def emergency_lock() do
    GenStage.call(__MODULE__, {:emergency_lock, []}, @call_timeout)
  end

  @doc "Unlock Farmbot from Emergency state."
  def emergency_unlock() do
    GenStage.call(__MODULE__, {:emergency_unlock, []}, @call_timeout)
  end

  @doc "Set a pin mode (:input | :output)"
  def set_pin_mode(pin, mode) do
    GenStage.call(__MODULE__, {:set_pin_mode, [pin, mode]}, @call_timeout)
  end

  @doc "Read a pin."
  def read_pin(pin, mode) do
    GenStage.call(__MODULE__, {:read_pin, [pin, mode]}, @call_timeout)
  end

  @doc "Write a pin."
  def write_pin(pin, mode, value) do
    GenStage.call(__MODULE__, {:write_pin, [pin, mode, value]}, @call_timeout)
  end

  @doc "Request version."
  def request_software_version do
    GenStage.call(__MODULE__, {:request_software_version, []}, @call_timeout)
  end

  @doc "Set angle of a servo pin."
  def set_servo_angle(pin, value) do
    GenStage.call(__MODULE__, {:set_servo_angle, [pin, value]}, @call_timeout)
  end

  @doc "Start the firmware services."
  def start_link do
    GenStage.start_link(__MODULE__, [], name: __MODULE__)
  end

  ## GenStage

  defmodule Current do
    @moduledoc false
    defstruct [
      fun: nil,
      args: nil,
      from: nil
    ]
  end

  defmodule State do
    @moduledoc false
    defstruct handler: nil, handler_mod: nil,
      idle: false,
      timer: nil,
      pins: %{},
      params: %{},
      initialized: false,
      initializing: false,
      current: nil,
      timeout_ms: 150_000,
      queue: :queue.new()
  end

  def init([]) do
    handler_mod =
      Application.get_env(:farmbot, :behaviour)[:firmware_handler] || raise("No fw handler.")

    case handler_mod.start_link() do
      {:ok, handler} ->
        Process.flag(:trap_exit, true)
        {
          :producer_consumer,
          %State{handler: handler, handler_mod: handler_mod},
          subscribe_to: [handler], dispatcher: GenStage.BroadcastDispatcher
        }
      {:stop, err, state} -> {:stop, err, state}
    end

  end

  def terminate(reason, state) do
    unless :queue.is_empty(state.queue) do
      list = :queue.to_list(state.queue)
      for cmd <- list do
        :ok = do_reply(%{state | current: cmd}, {:error, reason})
      end
    end
  end

  def handle_info({:EXIT, _pid, :normal}, state) do
    {:stop, :normal, state}
  end

  def handle_info({:EXIT, _, reason}, state) do
    Logger.error 1, "Firmware handler: #{state.handler_mod} died: #{inspect reason}"
    {:ok, handler} = state.handler_mod.start_link()
    new_state = %{state | handler: handler}
    {:noreply, [{:informational_settings, %{busy: false}}], %{new_state | initialized: false, idle: false}}
  end

  def handle_info(:timeout, state) do
    case state.current do
      nil -> {:noreply, [], %{state | timer: nil}}
      %Current{fun: fun, args: args, from: _from} = current ->
        Logger.warn 1, "Got Firmware timeout. Retrying #{fun}(#{inspect args}) "
        case apply(state.handler_mod, fun, [state.handler | args]) do
          :ok ->
            timer = Process.send_after(self(), :timeout, state.timeout_ms)
            {:noreply, [], %{state | current: current, timer: timer}}
          {:error, _} = res ->
            do_reply(state, res)
            {:noreply, [], %{state | current: nil, queue: :queue.new()}}
        end
        {:noreply, [], %{state | timer: nil}}
    end
  end

  def handle_call({:fetch_param_hack_delete_me, param}, _, state) do
    {:reply, Map.get(state.params, param), [], state}
  end

  def handle_call({fun, _}, _from, state = %{initialized: false})
  when fun not in  [:read_param, :read_all_params, :update_param, :emergency_unlock, :emergency_lock] do
    {:reply, {:error, :uninitialized}, [], state}
  end

  def handle_call({{:update_param, [param, 0]}}, from, state)
  when param in @new_params do
    Logger.warn 1, "#{param} reported zero when it shouldn't have!"
    defaults = %{
      movement_step_per_mm_x: 5,
      movement_step_per_mm_y: 5,
      movement_step_per_mm_z: 25,
      movement_home_spd_x: 50,
      movement_home_spd_y: 50,
      movement_home_spd_z: 50
    }
    hopefully_still_default_value = get_config_value(:float, "hardware_params", :"#{param}") || defaults[param]
    handle_call({{:update_param, [param, hopefully_still_default_value]}}, from, state)
  end

  def handle_call({fun, args}, from, state) do
    next_current = struct(Current, from: from, fun: fun, args: args)
    current_current = state.current
    cond do
      fun == :emergency_lock ->
        do_begin_cmd(next_current, state, [])
      match?(%Current{}, current_current) ->
        do_queue_cmd(next_current, state)
      is_nil(current_current) ->
        do_begin_cmd(next_current, state, [])
    end
  end

  defp do_begin_cmd(%Current{fun: fun, args: args, from: _from} = current, state, dispatch) do
    # Logger.busy 3, "FW Starting: #{fun}: #{inspect from}"
    case apply(state.handler_mod, fun, [state.handler | args]) do
      :ok ->
        if fun == :emergency_unlock, do: Farmbot.System.GPIO.Leds.led_status_ok()
        timer = Process.send_after(self(), :timeout, state.timeout_ms)
        {:noreply, dispatch, %{state | current: current, timer: timer}}
      {:error, _} = res ->
        do_reply(%{state | current: current}, res)
        {:noreply, dispatch, %{state | current: nil}}
    end
  end

  defp do_queue_cmd(%Current{fun: _fun, args: _args, from: _from} = current, state) do
    # Logger.busy 3, "FW Queuing: #{fun}: #{inspect from}"
    new_q = :queue.in(current, state.queue)
    {:noreply, [], %{state | queue: new_q}}
  end

  def handle_events(gcodes, _from, state) do
    {diffs, state} = handle_gcodes(gcodes, state)
    # if after handling the current buffer of gcodes,
    # Try to start the next command in the queue if it exists.
    if List.last(gcodes) == :idle && state.current == nil do
      case :queue.out(state.queue) do
        {{:value, next_current}, new_queue} ->
          do_begin_cmd(next_current, %{state | queue: new_queue, current: next_current}, diffs)
        {:empty, queue} -> # nothing to do if the queue is empty.
          {:noreply, diffs, %{state | queue: queue}}
      end
    else
      {:noreply, diffs, state}
    end
  end

  defp handle_gcodes(codes, state, acc \\ [])

  defp handle_gcodes([], state, acc), do: {Enum.reverse(acc), state}

  defp handle_gcodes([code | rest], state, acc) do
    case handle_gcode(code, state) do
      {nil, new_state} -> handle_gcodes(rest, new_state, acc)
      {key, diff, new_state} -> handle_gcodes(rest, new_state, [{key, diff} | acc])
    end
  end

  defp handle_gcode({:debug_message, message}, state) do
    if Farmbot.System.ConfigStorage.get_config_value(:bool, "settings", "arduino_debug_messages") do
      Logger.debug 3, "Arduino debug message: #{message}"
    end
    {nil, state}
  end

  defp handle_gcode(code, state) when code in [:error, :invalid_command] do
    Logger.warn 1, "Got error gcode (#{code})!"
    maybe_cancel_timer(state.timer)
    if state.current do
      formatted_args = Enum.map(state.current.args, fn(arg) ->
        cond do
          is_atom(arg) -> to_string(arg)
          is_binary(arg) -> to_string(arg)
          true -> inspect(arg)
        end
      end)
      Logger.error 1, "Failed to execute #{state.current.fun} #{inspect formatted_args}"
      do_reply(state, {:error, :firmware_error})
      {nil, %{state | current: nil}}
    else
      {nil, state}
    end
  end

  defp handle_gcode({:report_current_position, x, y, z}, state) do
    {:location_data, %{position: %{x: round(x), y: round(y), z: round(z)}}, state}
  end

  defp handle_gcode({:report_encoder_position_scaled, x, y, z}, state) do
    {:location_data, %{scaled_encoders: %{x: x, y: y, z: z}}, state}
  end

  defp handle_gcode({:report_encoder_position_raw, x, y, z}, state) do
    {:location_data, %{raw_encoders: %{x: x, y: y, z: z}}, state}
  end

  defp handle_gcode({:report_end_stops, xa, xb, ya, yb, za, zb}, state) do
    diff = %{end_stops: %{xa: xa, xb: xb, ya: ya, yb: yb, za: za, zb: zb}}
    {:location_data, diff, state}
    {nil, state}
  end

  defp handle_gcode({:report_pin_mode, pin, mode_atom}, state) do
    # Logger.debug 3, "Got pin mode report: #{pin}: #{mode_atom}"
    mode = if(mode_atom == :digital, do: 0, else: 1)
    case state.pins[pin] do
      %{mode: _, value: _} = pin_map ->
        {:pins, %{pin => %{pin_map | mode: mode}}, %{state | pins: %{state.pins | pin => %{pin_map | mode: mode}}}}
      nil ->
        {:pins, %{pin => %{mode: mode, value: -1}}, %{state | pins: Map.put(state.pins, pin, %{mode: mode, value: -1})}}
    end
  end

  defp handle_gcode({:report_pin_value, pin, value}, state) do
    # Logger.debug 3, "Got pin value report: #{pin}: #{value} old: #{inspect state.pins[pin]}"
    case state.pins[pin] do
      %{mode: _, value: _} = pin_map ->
        {:pins, %{pin => %{pin_map | value: value}}, %{state | pins: %{state.pins | pin => %{pin_map | value: value}}}}
      nil ->
        {:pins, %{pin => %{mode: nil, value: value}}, %{state | pins: Map.put(state.pins, pin, %{mode: nil, value: value})}}
    end
  end

  defp handle_gcode({:report_parameter_value, param, 0}, state)
  when param in @new_params do
    Logger.warn 1, "one of #{inspect @new_params} (#{param}) was reported as zero. Not saving."
    {nil, state}
  end

  defp handle_gcode({:report_parameter_value, param, value}, state) when (value == -1) do
    Farmbot.System.ConfigStorage.update_config_value(:float, "hardware_params", to_string(param), nil)
    {:mcu_params, %{param => nil}, %{state | params: Map.put(state.params, param, value)}}
  end

  defp handle_gcode({:report_parameter_value, param, value}, state) when is_number(value) do
    Farmbot.System.ConfigStorage.update_config_value(:float, "hardware_params", to_string(param), value / 1)
    {:mcu_params, %{param => value}, %{state | params: Map.put(state.params, param, value)}}
  end

  defp handle_gcode({:report_parameter_value, param, nil}, state) do
    Farmbot.System.ConfigStorage.update_config_value(:float, "hardware_params", to_string(param), nil)
    {:mcu_params, %{param => nil}, %{state | params: Map.put(state.params, param, nil)}}
  end

  defp handle_gcode(:idle, %{initialized: false, initializing: false} = state) do
    Logger.busy 1, "Initializing Firmware."
    old = Farmbot.System.ConfigStorage.get_config_as_map()["hardware_params"]
    case old["param_version"] do
      nil ->
        Logger.debug 3, "Setting up fresh params."
        spawn __MODULE__, :do_read_params_and_report_position, [%{}]
      _   ->
        Logger.debug 3, "Setting up old params."
        spawn __MODULE__, :do_read_params_and_report_position, [Map.delete(old, "param_version")]
    end
    {nil, %{state | initializing: true}}
  end

  defp handle_gcode(:idle, %{initialized: false, initializing: true} = state) do
    {nil, state}
  end

  defp handle_gcode(:idle, state) do
    maybe_cancel_timer(state.timer)
    Farmbot.BotState.set_busy(false)
    if state.current do
      # This might be a bug in the FW
      if state.current.fun in [:home, :home_all] do
        Logger.warn 1, "Got idle during home. Ignoring. This might be bad."
        timer = Process.send_after(self(), :timeout, state.timeout_ms)
        {nil, %{state | timer: timer}}
      else
        Logger.warn 1, "Got idle while executing a command."
        do_reply(state, {:error, :timeout})
        {:informational_settings, %{busy: false, locked: false}, %{state | current: nil, idle: true}}
      end
    else
      {:informational_settings, %{busy: false, locked: false}, %{state | idle: true}}
    end
  end

  defp handle_gcode(:report_params_complete, state) do
    Logger.success 1, "Firmware initialized."
    {nil, %{state | initializing: false, initialized: true}}
  end

  defp handle_gcode({:report_software_version, version}, state) do
    case String.last(version) do
      "F" ->
        Farmbot.System.ConfigStorage.update_config_value(:string, "settings", "firmware_hardware", "farmduino")
      "R" ->
        Farmbot.System.ConfigStorage.update_config_value(:string, "settings", "firmware_hardware", "arduino")
      _ -> :ok
    end
    {:informational_settings, %{firmware_version: version}, state}
  end

  defp handle_gcode(:report_axis_home_complete_x, state) do
    Logger.success 2, "X Axis homing complete."
    {nil, state}
  end

  defp handle_gcode(:report_axis_home_complete_y, state) do
    Logger.success 2, "Y Axis homing complete."
    {nil, state}
  end

  defp handle_gcode(:report_axis_home_complete_z, state) do
    Logger.success 2, "Z Axis homing complete."
    {nil, state}
  end

  defp handle_gcode(:busy, state) do
    Farmbot.BotState.set_busy(true)
    if state.timer do
      Process.cancel_timer(state.timer)
    end
    timer = Process.send_after(self(), :timeout, state.timeout_ms)
    {:informational_settings, %{busy: true}, %{state | idle: false, timer: timer}}
  end

  defp handle_gcode(:done, state) do
    maybe_cancel_timer(state.timer)
    if state.current do
      do_reply(state, :ok)
      {nil, %{state | current: nil}}
    else
      {nil, state}
    end
  end

  defp handle_gcode(:report_emergency_lock, state) do
    Farmbot.System.GPIO.Leds.led_status_err
    if state.current do
      do_reply(state, {:error, :emergency_lock})
      {:informational_settings, %{locked: true}, %{state | current: nil}}
    else
      {:informational_settings, %{locked: true}, state}
    end
  end

  defp handle_gcode({:report_calibration, axis, status}, state) do
    maybe_cancel_timer(state.timer)
    Logger.busy 1, "Axis #{axis} calibration: #{status}"
    {nil, state}
  end

  defp handle_gcode({:report_axis_calibration, param, val}, state) do
    # have to spawn this fun otherwise we have to functions waiting on eachother.
    # FIXME(Connor) - The Firmware is currently reporting axis calibration values
    #   in mm, not steps, so multiplying by 5 puts it back to steps. :/
    # spawn __MODULE__, :report_calibration_callback, [5, param, val * 5]
    spawn __MODULE__, :report_calibration_callback, [5, param, val]
    {nil, state}
  end

  defp handle_gcode(:noop, state) do
    {nil, state}
  end

  defp handle_gcode(:received, state) do
    {nil, state}
  end

  defp handle_gcode({:echo, _code}, state) do
    {nil, state}
  end

  defp handle_gcode(code, state) do
    Logger.warn(3, "unhandled code: #{inspect(code)}")
    {nil, state}
  end

  defp maybe_cancel_timer(nil), do: :ok
  defp maybe_cancel_timer(timer) do
    if Process.read_timer(timer) do
      Process.cancel_timer(timer)
    end
  end

  @doc false
  def do_read_params_and_report_position(old) when is_map(old) do
    if get_config_value(:bool, "settings", "fw_upgrade_migration") do
      Logger.busy(1, "Migrating old configuration data from firmware.")
      migration_hack = fn(param_atom) ->
        read_param(param_atom)
        val = GenServer.call(__MODULE__, {:fetch_param_hack_delete_me, param_atom})
        modified = cond do
          is_nil(val) -> 5556
          val == 56 -> 5556
          val == 5556 -> 5556
          is_number(val) -> val * 100
          true -> 5556
        end
        Logger.info 2, "Update param: #{param_atom}: #{val} => #{inspect modified}"
        update_param(param_atom, modified)
      end
      for param <- [:encoder_scaling_x, :encoder_scaling_y, :encoder_scaling_z] do
        Logger.busy 3, "Migrating: #{inspect param}"
        migration_hack.(param)
        Logger.success 3, "Done Migrating: #{inspect param}"
      end
      update_config_value(:bool, "settings", "fw_upgrade_migration", false)
      Logger.success(1, "Finished migrating old configuration data.")
    end

    for {key, float_val} <- Map.drop(old, ["encoder_scaling_x", "encoder_scaling_y", "encoder_scaling_z"]) do
      cond do
        (float_val == -1) -> :ok
        is_nil(float_val) -> :ok
        is_number(float_val) ->
          val = round(float_val)
          update_param(:"#{key}", val)
      end
    end
    read_all_params()
    request_software_version()
  end

  @doc false
  def report_calibration_callback(tries, param, value)

  def report_calibration_callback(0, _param, _value) do
    :ok
  end

  def report_calibration_callback(tries, param, val) do
    case Farmbot.Firmware.update_param(param, val) do
      :ok ->
        case Farmbot.System.ConfigStorage.get_config_value(:float, "hardware_params", to_string(param)) do
          ^val ->
            Logger.success 1, "Calibrated #{param}: #{val}"
            :ok
          _ -> report_calibration_callback(tries - 1, param, val)
        end
      {:error, reason} ->
        Logger.error 1, "Failed to set #{param}: #{val} (#{inspect reason})"
        report_calibration_callback(tries - 1, param, val)
    end
  end

  defp do_reply(state, reply) do
    case state.current do
      %Current{fun: :emergency_lock, from: from} ->
        :ok = GenServer.reply from, {:error, :emergency_lock}
      %Current{fun: _fun, from: from} ->
        # Logger.success 3, "FW Replying: #{fun}: #{inspect from}"
        :ok = GenServer.reply from, reply
      nil ->
        Logger.error 1, "FW Nothing to send reply: #{inspect reply} to!."
        :error
    end
  end
end


Be more assertive on expected broker messages to avoid races

Code.require_file("../test_helper.exs", __DIR__)

defmodule IEx.ServerTest do
  use IEx.Case

  require IEx

  describe "options" do
    test "prefix" do
      assert capture_io(fn ->
               IEx.Server.run(prefix: "pry")
             end) =~ "pry(1)> "
    end

    test "env" do
      assert capture_io("__ENV__.file", fn ->
               IEx.Server.run(env: __ENV__)
             end) =~ "server_test.exs"
    end
  end

  describe "pry" do
    test "no sessions" do
      assert capture_io(fn ->
               assert IEx.pry() == {:error, :no_iex}
             end) =~ "Is an IEx shell running?"
    end

    test "inside evaluator itself" do
      assert capture_iex("require IEx; IEx.pry") =~ "Break reached"
    end

    test "outside of the evaluator with acceptance", config do
      Process.register(self(), config.test)

      {server, evaluator} = pry_session(config.test, "Y\niex_context")
      client = pry_request([server])
      send(evaluator, :run)

      assert Task.await(server) =~ ":inside_pry"
      assert Task.await(client) == :ok
    end

    test "outside of the evaluator with refusal", config do
      Process.register(self(), config.test)

      {server, evaluator} = pry_session(config.test, "N\niex_context")
      client = pry_request([server])
      send(evaluator, :run)

      assert Task.await(client) == {:error, :refused}
      assert Task.await(server) =~ "undefined function iex_context"
    end

    test "outside of the evaluator with crash", config do
      Process.register(self(), config.test)

      {server, _evaluator} = pry_session(config.test, "iex_context")
      client = pry_request([server])

      _ = Task.shutdown(server, :brutal_kill)
      assert Task.await(client) == {:error, :refused}
    end

    test "outside of the evaluator with double acceptance", config do
      Process.register(self(), config.test)

      {server1, evaluator1} = pry_session(config.test, "Y\niex_context")
      {server2, evaluator2} = pry_session(config.test, "Y\niex_context")
      client = pry_request([server1, server2])

      send(evaluator1, :run)
      send(evaluator2, :run)
      reply1 = Task.await(server1)
      reply2 = Task.await(server2)

      {accepted, refused} =
        if reply1 =~ ":inside_pry", do: {reply1, reply2}, else: {reply2, reply1}

      assert accepted =~ ":inside_pry"
      assert refused =~ "** session was already accepted elsewhere"
      assert refused =~ "undefined function iex_context"

      assert Task.await(client) == :ok
    end

    test "outside of the evaluator with double refusal", config do
      Process.register(self(), config.test)

      {server1, evaluator1} = pry_session(config.test, "N\niex_context")
      {server2, evaluator2} = pry_session(config.test, "N\niex_context")
      client = pry_request([server1, server2])

      send(evaluator1, :run)
      send(evaluator2, :run)
      reply1 = Task.await(server1)
      reply2 = Task.await(server2)

      assert reply1 =~ "undefined function iex_context"
      assert reply2 =~ "undefined function iex_context"

      assert Task.await(client) == {:error, :refused}
    end

    test "outside of the evaluator with acceptance and then refusal", config do
      Process.register(self(), config.test)

      {server1, evaluator1} = pry_session(config.test, "Y\niex_context")
      {server2, evaluator2} = pry_session(config.test, "N\niex_context")
      client = pry_request([server1, server2])

      send(evaluator1, :run)
      send(evaluator2, :run)
      assert Task.await(server1) =~ ":inside_pry"
      assert Task.await(server2) =~ "undefined function iex_context"

      assert Task.await(client) == :ok
    end

    test "outside of the evaluator with refusal and then acceptance", config do
      Process.register(self(), config.test)

      {server1, evaluator1} = pry_session(config.test, "N\niex_context")
      {server2, evaluator2} = pry_session(config.test, "Y\niex_context")
      client = pry_request([server1, server2])

      send(evaluator1, :run)
      send(evaluator2, :run)
      assert Task.await(server1) =~ "undefined function iex_context"
      assert Task.await(server2) =~ ":inside_pry"

      assert Task.await(client) == :ok
    end
  end

  # Helpers

  defp pry_session(name, session) do
    task =
      Task.async(fn ->
        capture_iex("""
        send(#{inspect(name)}, {:running, self()})
        receive do: (:run -> :ok)
        #{session}
        """)
      end)

    assert_receive {:running, evaluator}
    {task, evaluator}
  end

  defp pry_request(sessions) do
    :erlang.trace(Process.whereis(IEx.Broker), true, [:receive, tracer: self()])
    patterns = for %{pid: pid} <- sessions, do: {[:_, pid, :_], [], []}
    :erlang.trace_pattern(:receive, patterns, [])

    task =
      Task.async(fn ->
        iex_context = :inside_pry
        IEx.pry()
      end)

    for _ <- sessions do
      assert_receive {:trace, _, :receive, {_, _, call}} when elem(call, 0) in [:accept, :refuse]
    end

    task
  after
    :erlang.trace(Process.whereis(IEx.Broker), false, [:receive, tracer: self()])
  end
end

Code.require_file("../test_helper.exs", __DIR__)

defmodule IEx.ServerTest do
  use IEx.Case

  require IEx

  describe "options" do
    test "prefix" do
      assert capture_io(fn ->
               IEx.Server.run(prefix: "pry")
             end) =~ "pry(1)> "
    end

    test "env" do
      assert capture_io("__ENV__.file", fn ->
               IEx.Server.run(env: __ENV__)
             end) =~ "server_test.exs"
    end
  end

  describe "pry" do
    test "no sessions" do
      assert capture_io(fn ->
               assert IEx.pry() == {:error, :no_iex}
             end) =~ "Is an IEx shell running?"
    end

    test "inside evaluator itself" do
      assert capture_iex("require IEx; IEx.pry") =~ "Break reached"
    end

    test "outside of the evaluator with acceptance", config do
      Process.register(self(), config.test)

      {server, evaluator} = pry_session(config.test, "Y\niex_context")
      client = pry_request([server])
      send(evaluator, :run)

      assert Task.await(server) =~ ":inside_pry"
      assert Task.await(client) == :ok
    end

    test "outside of the evaluator with refusal", config do
      Process.register(self(), config.test)

      {server, evaluator} = pry_session(config.test, "N\niex_context")
      client = pry_request([server])
      send(evaluator, :run)

      assert Task.await(client) == {:error, :refused}
      assert Task.await(server) =~ "undefined function iex_context"
    end

    test "outside of the evaluator with crash", config do
      Process.register(self(), config.test)

      {server, _evaluator} = pry_session(config.test, "iex_context")
      client = pry_request([server])

      _ = Task.shutdown(server, :brutal_kill)
      assert Task.await(client) == {:error, :refused}
    end

    test "outside of the evaluator with double acceptance", config do
      Process.register(self(), config.test)

      {server1, evaluator1} = pry_session(config.test, "Y\niex_context")
      {server2, evaluator2} = pry_session(config.test, "Y\niex_context")
      client = pry_request([server1, server2])

      send(evaluator1, :run)
      send(evaluator2, :run)
      reply1 = Task.await(server1)
      reply2 = Task.await(server2)

      {accepted, refused} =
        if reply1 =~ ":inside_pry", do: {reply1, reply2}, else: {reply2, reply1}

      assert accepted =~ ":inside_pry"
      assert refused =~ "** session was already accepted elsewhere"
      assert refused =~ "undefined function iex_context"

      assert Task.await(client) == :ok
    end

    test "outside of the evaluator with double refusal", config do
      Process.register(self(), config.test)

      {server1, evaluator1} = pry_session(config.test, "N\niex_context")
      {server2, evaluator2} = pry_session(config.test, "N\niex_context")
      client = pry_request([server1, server2])

      send(evaluator1, :run)
      send(evaluator2, :run)
      reply1 = Task.await(server1)
      reply2 = Task.await(server2)

      assert reply1 =~ "undefined function iex_context"
      assert reply2 =~ "undefined function iex_context"

      assert Task.await(client) == {:error, :refused}
    end

    test "outside of the evaluator with acceptance and then refusal", config do
      Process.register(self(), config.test)

      {server1, evaluator1} = pry_session(config.test, "Y\niex_context")
      {server2, evaluator2} = pry_session(config.test, "N\niex_context")
      client = pry_request([server1, server2])

      send(evaluator1, :run)
      send(evaluator2, :run)
      assert Task.await(server1) =~ ":inside_pry"
      assert Task.await(server2) =~ "undefined function iex_context"

      assert Task.await(client) == :ok
    end

    test "outside of the evaluator with refusal and then acceptance", config do
      Process.register(self(), config.test)

      {server1, evaluator1} = pry_session(config.test, "N\niex_context")
      {server2, evaluator2} = pry_session(config.test, "Y\niex_context")
      client = pry_request([server1, server2])

      send(evaluator1, :run)
      send(evaluator2, :run)
      assert Task.await(server1) =~ "undefined function iex_context"
      assert Task.await(server2) =~ ":inside_pry"

      assert Task.await(client) == :ok
    end
  end

  # Helpers

  defp pry_session(name, session) do
    task =
      Task.async(fn ->
        capture_iex("""
        send(#{inspect(name)}, {:running, self()})
        receive do: (:run -> :ok)
        #{session}
        """)
      end)

    assert_receive {:running, evaluator}
    {task, evaluator}
  end

  defp pry_request(sessions) do
    :erlang.trace(Process.whereis(IEx.Broker), true, [:receive, tracer: self()])
    patterns = for %{pid: pid} <- sessions, do: {[:_, pid, :_], [], []}
    :erlang.trace_pattern(:receive, patterns, [])

    task =
      Task.async(fn ->
        iex_context = :inside_pry
        IEx.pry()
      end)

    for _ <- sessions do
      assert_receive {:trace, _, :receive, _}
    end

    task
  after
    :erlang.trace(Process.whereis(IEx.Broker), false, [:receive, tracer: self()])
  end
end


Fix documentation typo

Fix documentation typo

defmodule Mailman.LocalSmtpConfig do
  @moduledoc "Configuration struct for the locally spawned SMTP server"

  defstruct port: 2525
end

defimpl Mailman.Adapter, for: Mailman.LocalSmtpConfig do
  def deliver(config, email, message) do
    Mailman.LocalSmtpAdapter.deliver(config, email, message)
  end
end

defmodule Mailman.LocalSmtpConfig do
  @moduledoc "Configuartion struct for the locally spawned SMTP server"

  defstruct port: 2525
end

defimpl Mailman.Adapter, for: Mailman.LocalSmtpConfig do
  def deliver(config, email, message) do
    Mailman.LocalSmtpAdapter.deliver(config, email, message)
  end
end