From f78ce3773c174732b0d100392cadcc5d89467fb9 Mon Sep 17 00:00:00 2001 From: Connor Rigby Date: Mon, 23 Oct 2017 19:14:30 -0700 Subject: [PATCH] [BROKEN] begin allowing http adapter to be replaced. --- .gitignore | 1 + config/config.exs | 1 - lib/farmbot.ex | 2 +- lib/farmbot/bot_state/transport/gen_mqtt.ex | 2 +- lib/farmbot/firmware/firmware.ex | 30 +- lib/farmbot/firmware/gcode.ex | 8 - lib/farmbot/firmware/gcode/param.ex | 270 ++++++++++ lib/farmbot/firmware/gcode/parser.ex | 277 +--------- lib/farmbot/firmware/handler.ex | 57 ++ .../{ => uart_handler}/uart_handler.ex | 0 lib/farmbot/firmware/vec3.ex | 11 + lib/farmbot/http/adapter.ex | 2 + lib/farmbot/http/http.ex | 499 +----------------- lib/farmbot/http/httpoison_adapter.ex | 493 +++++++++++++++++ lib/farmbot/http/supervisor.ex | 2 +- 15 files changed, 865 insertions(+), 790 deletions(-) delete mode 100644 lib/farmbot/firmware/gcode.ex create mode 100644 lib/farmbot/firmware/gcode/param.ex create mode 100644 lib/farmbot/firmware/handler.ex rename lib/farmbot/firmware/{ => uart_handler}/uart_handler.ex (100%) create mode 100644 lib/farmbot/firmware/vec3.ex create mode 100644 lib/farmbot/http/adapter.ex create mode 100644 lib/farmbot/http/httpoison_adapter.ex diff --git a/.gitignore b/.gitignore index aeae8c17..582e4900 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,4 @@ tmp *.sqlite3-wal *.img run-qemu.sh +auth_secret.exs diff --git a/config/config.exs b/config/config.exs index c68dc7c7..a1a35ea3 100644 --- a/config/config.exs +++ b/config/config.exs @@ -11,7 +11,6 @@ config :iex, :colors, enabled: true config :ssl, protocol_version: :"tlsv1.2" - # This is usually in the `priv` dir of :tzdata, but our fs is read only. config :tzdata, :data_dir, "/tmp" config :tzdata, :autoupdate, :disabled diff --git a/lib/farmbot.ex b/lib/farmbot.ex index d24f0f2c..1b0952a2 100644 --- a/lib/farmbot.ex +++ b/lib/farmbot.ex @@ -41,7 +41,7 @@ defmodule Farmbot do def start(type, start_opts) def start(_, start_opts) do - Logger.info(">> Booting Farmbot OS version: #{@version} - #{@commit}") + Logger.debug("Booting Farmbot OS version: #{@version} - #{@commit}") name = Keyword.get(start_opts, :name, __MODULE__) case Supervisor.start_link(__MODULE__, [], name: name) do diff --git a/lib/farmbot/bot_state/transport/gen_mqtt.ex b/lib/farmbot/bot_state/transport/gen_mqtt.ex index 2c93f9e6..f4f75308 100644 --- a/lib/farmbot/bot_state/transport/gen_mqtt.ex +++ b/lib/farmbot/bot_state/transport/gen_mqtt.ex @@ -53,7 +53,7 @@ defmodule Farmbot.BotState.Transport.GenMQTT do def on_connect(state) do GenMQTT.subscribe(self(), [{bot_topic(state.device), 0}]) - Logger.info(">> Connected!") + Logger.info("Connected!") if state.cache do GenMQTT.publish(self(), status_topic(state.device), state.cache, 0, false) diff --git a/lib/farmbot/firmware/firmware.ex b/lib/farmbot/firmware/firmware.ex index c421e244..53a67e49 100644 --- a/lib/farmbot/firmware/firmware.ex +++ b/lib/farmbot/firmware/firmware.ex @@ -4,33 +4,23 @@ defmodule Farmbot.Firmware do use GenStage require Logger - defmodule Handler do - @moduledoc """ - Any module that implements this behaviour should be a GenStage. - - The implementng stage should communicate with the various Farmbot - hardware such as motors and encoders. The `Farmbot.Firmware` module - will subscribe_to: the implementing handler. Events should be - Gcodes as parsed by `Farmbot.Firmware.Gcode.Parser`. - """ - - @doc "Start a firmware handler." - @callback start_link :: GenServer.on_start() - - @doc "Write a gcode." - @callback write(Farmbot.Firmware.Gcode.t()) :: :ok | {:error, term} - end - @handler Application.get_env(:farmbot, :behaviour)[:firmware_handler] || raise("No fw handler.") + defdelegate move_absolute(vec3), to: @handler + defdelegate calibrate(axis), to: @handler + defdelegate update_param(param, val), to: @handler + defdelegate read_param(param), to: @handler + defdelegate emergency_lock(), to: @handler + defdelegate emergency_unlock(), to: @handler + defdelegate find_home(axis), to: @handler + defdelegate read_pin(pin, mode), to: @handler + defdelegate write_pin(pin, mode, value), to: @handler + @doc "Start the firmware services." def start_link do GenStage.start_link(__MODULE__, [], name: __MODULE__) end - @doc "Writes a Gcode to a the running hand:ler" - def write(code), do: @handler.write(code) - ## GenStage defmodule State do diff --git a/lib/farmbot/firmware/gcode.ex b/lib/farmbot/firmware/gcode.ex deleted file mode 100644 index 70caf8ba..00000000 --- a/lib/farmbot/firmware/gcode.ex +++ /dev/null @@ -1,8 +0,0 @@ -defmodule Farmbot.Firmware.Gcode do - @moduledoc """ - Gcode is the itermediate representation - of commands to the underlying hardware. - """ - @typedoc "Code representation." - @type t :: term -end diff --git a/lib/farmbot/firmware/gcode/param.ex b/lib/farmbot/firmware/gcode/param.ex new file mode 100644 index 00000000..d7080f19 --- /dev/null +++ b/lib/farmbot/firmware/gcode/param.ex @@ -0,0 +1,270 @@ +defmodule Farmbot.Firmware.Gcode.Param do + @moduledoc "Firmware paramaters." + + @typedoc "Human readable name of a paramater." + @type t :: atom + + @doc ~S""" + Parses farmbot_arduino_firmware params. + If we want the name of param "0"\n + Example: + iex> Gcode.parse_param("0") + :param_version + + Example: + iex> Gcode.parse_param(0) + :param_version + + If we want the integer of param :param_version\n + Example: + iex> Gcode.parse_param(:param_version) + 0 + + Example: + iex> Gcode.parse_param("param_version") + 0 + """ + @spec parse_param(binary | integer) :: t | nil + def parse_param("0"), do: :param_version + + def parse_param("2"), do: :param_config_ok + def parse_param("3"), do: :param_use_eeprom + def parse_param("4"), do: :param_e_stop_on_mov_err + def parse_param("5"), do: :param_mov_nr_retry + + def parse_param("11"), do: :movement_timeout_x + def parse_param("12"), do: :movement_timeout_y + def parse_param("13"), do: :movement_timeout_z + + def parse_param("15"), do: :movement_keep_active_x + def parse_param("16"), do: :movement_keep_active_y + def parse_param("17"), do: :movement_keep_active_z + + def parse_param("18"), do: :movement_home_at_boot_x + def parse_param("19"), do: :movement_home_at_boot_y + def parse_param("20"), do: :movement_home_at_boot_z + + def parse_param("21"), do: :movement_invert_endpoints_x + def parse_param("22"), do: :movement_invert_endpoints_y + def parse_param("23"), do: :movement_invert_endpoints_z + + def parse_param("25"), do: :movement_enable_endpoints_x + def parse_param("26"), do: :movement_enable_endpoints_y + def parse_param("27"), do: :movement_enable_endpoints_z + + def parse_param("31"), do: :movement_invert_motor_x + def parse_param("32"), do: :movement_invert_motor_y + def parse_param("33"), do: :movement_invert_motor_z + + def parse_param("36"), do: :movement_secondary_motor_x + def parse_param("37"), do: :movement_secondary_motor_invert_x + + def parse_param("41"), do: :movement_steps_acc_dec_x + def parse_param("42"), do: :movement_steps_acc_dec_y + def parse_param("43"), do: :movement_steps_acc_dec_z + + def parse_param("45"), do: :movement_stop_at_home_x + def parse_param("46"), do: :movement_stop_at_home_y + def parse_param("47"), do: :movement_stop_at_home_z + + def parse_param("51"), do: :movement_home_up_x + def parse_param("52"), do: :movement_home_up_y + def parse_param("53"), do: :movement_home_up_z + + def parse_param("55"), do: :movement_step_per_mm_x + def parse_param("56"), do: :movement_step_per_mm_y + def parse_param("57"), do: :movement_step_per_mm_z + + def parse_param("61"), do: :movement_min_spd_x + def parse_param("62"), do: :movement_min_spd_y + def parse_param("63"), do: :movement_min_spd_z + + def parse_param("65"), do: :movement_home_speed_x + def parse_param("66"), do: :movement_home_speed_y + def parse_param("67"), do: :movement_home_speed_z + + def parse_param("71"), do: :movement_max_spd_x + def parse_param("72"), do: :movement_max_spd_y + def parse_param("73"), do: :movement_max_spd_z + + def parse_param("101"), do: :encoder_enabled_x + def parse_param("102"), do: :encoder_enabled_y + def parse_param("103"), do: :encoder_enabled_z + + def parse_param("105"), do: :encoder_type_x + def parse_param("106"), do: :encoder_type_y + def parse_param("107"), do: :encoder_type_z + + def parse_param("111"), do: :encoder_missed_steps_max_x + def parse_param("112"), do: :encoder_missed_steps_max_y + def parse_param("113"), do: :encoder_missed_steps_max_z + + def parse_param("115"), do: :encoder_scaling_x + def parse_param("116"), do: :encoder_scaling_y + def parse_param("117"), do: :encoder_scaling_z + + def parse_param("121"), do: :encoder_missed_steps_decay_x + def parse_param("122"), do: :encoder_missed_steps_decay_y + def parse_param("123"), do: :encoder_missed_steps_decay_z + + def parse_param("125"), do: :encoder_use_for_pos_x + def parse_param("126"), do: :encoder_use_for_pos_y + def parse_param("127"), do: :encoder_use_for_pos_z + + def parse_param("131"), do: :encoder_invert_x + def parse_param("132"), do: :encoder_invert_y + def parse_param("133"), do: :encoder_invert_z + + def parse_param("141"), do: :movement_axis_nr_steps_x + def parse_param("142"), do: :movement_axis_nr_steps_y + def parse_param("143"), do: :movement_axis_nr_steps_z + + def parse_param("145"), do: :movement_stop_at_max_x + def parse_param("146"), do: :movement_stop_at_max_y + def parse_param("147"), do: :movement_stop_at_max_z + + def parse_param("201"), do: :pin_guard_1_pin_nr + def parse_param("202"), do: :pin_guard_1_pin_time_out + def parse_param("203"), do: :pin_guard_1_active_state + + def parse_param("205"), do: :pin_guard_2_pin_nr + def parse_param("206"), do: :pin_guard_2_pin_time_out + def parse_param("207"), do: :pin_guard_2_active_state + + def parse_param("211"), do: :pin_guard_3_pin_nr + def parse_param("212"), do: :pin_guard_3_pin_time_out + def parse_param("213"), do: :pin_guard_3_active_state + + def parse_param("215"), do: :pin_guard_4_pin_nr + def parse_param("216"), do: :pin_guard_4_pin_time_out + def parse_param("217"), do: :pin_guard_4_active_state + + def parse_param("221"), do: :pin_guard_5_pin_nr + def parse_param("222"), do: :pin_guard_5_time_out + def parse_param("223"), do: :pin_guard_5_active_state + def parse_param(param) when is_integer(param), do: parse_param("#{param}") + + @spec parse_param(t) :: integer | nil + def parse_param(:param_version), do: 0 + + def parse_param(:param_config_ok), do: 2 + def parse_param(:param_use_eeprom), do: 3 + def parse_param(:param_e_stop_on_mov_err), do: 4 + def parse_param(:param_mov_nr_retry), do: 5 + + def parse_param(:movement_timeout_x), do: 11 + def parse_param(:movement_timeout_y), do: 12 + def parse_param(:movement_timeout_z), do: 13 + + def parse_param(:movement_keep_active_x), do: 15 + def parse_param(:movement_keep_active_y), do: 16 + def parse_param(:movement_keep_active_z), do: 17 + + def parse_param(:movement_home_at_boot_x), do: 18 + def parse_param(:movement_home_at_boot_y), do: 19 + def parse_param(:movement_home_at_boot_z), do: 20 + + def parse_param(:movement_invert_endpoints_x), do: 21 + def parse_param(:movement_invert_endpoints_y), do: 22 + def parse_param(:movement_invert_endpoints_z), do: 23 + + def parse_param(:movement_invert_motor_x), do: 31 + def parse_param(:movement_invert_motor_y), do: 32 + def parse_param(:movement_invert_motor_z), do: 33 + + def parse_param(:movement_enable_endpoints_x), do: 25 + def parse_param(:movement_enable_endpoints_y), do: 26 + def parse_param(:movement_enable_endpoints_z), do: 27 + + def parse_param(:movement_secondary_motor_x), do: 36 + def parse_param(:movement_secondary_motor_invert_x), do: 37 + + def parse_param(:movement_steps_acc_dec_x), do: 41 + def parse_param(:movement_steps_acc_dec_y), do: 42 + def parse_param(:movement_steps_acc_dec_z), do: 43 + + def parse_param(:movement_stop_at_home_x), do: 45 + def parse_param(:movement_stop_at_home_y), do: 46 + def parse_param(:movement_stop_at_home_z), do: 47 + + def parse_param(:movement_home_up_x), do: 51 + def parse_param(:movement_home_up_y), do: 52 + def parse_param(:movement_home_up_z), do: 53 + + def parse_param(:movement_step_per_mm_x), do: 55 + def parse_param(:movement_step_per_mm_y), do: 56 + def parse_param(:movement_step_per_mm_z), do: 57 + + def parse_param(:movement_min_spd_x), do: 61 + def parse_param(:movement_min_spd_y), do: 62 + def parse_param(:movement_min_spd_z), do: 63 + + def parse_param(:movement_home_speed_x), do: 65 + def parse_param(:movement_home_speed_y), do: 66 + def parse_param(:movement_home_speed_z), do: 67 + + def parse_param(:movement_max_spd_x), do: 71 + def parse_param(:movement_max_spd_y), do: 72 + def parse_param(:movement_max_spd_z), do: 73 + + def parse_param(:encoder_enabled_x), do: 101 + def parse_param(:encoder_enabled_y), do: 102 + def parse_param(:encoder_enabled_z), do: 103 + + def parse_param(:encoder_type_x), do: 105 + def parse_param(:encoder_type_y), do: 106 + def parse_param(:encoder_type_z), do: 107 + + def parse_param(:encoder_missed_steps_max_x), do: 111 + def parse_param(:encoder_missed_steps_max_y), do: 112 + def parse_param(:encoder_missed_steps_max_z), do: 113 + + def parse_param(:encoder_scaling_x), do: 115 + def parse_param(:encoder_scaling_y), do: 116 + def parse_param(:encoder_scaling_z), do: 117 + + def parse_param(:encoder_missed_steps_decay_x), do: 121 + def parse_param(:encoder_missed_steps_decay_y), do: 122 + def parse_param(:encoder_missed_steps_decay_z), do: 123 + + def parse_param(:encoder_use_for_pos_x), do: 125 + def parse_param(:encoder_use_for_pos_y), do: 126 + def parse_param(:encoder_use_for_pos_z), do: 127 + + def parse_param(:encoder_invert_x), do: 131 + def parse_param(:encoder_invert_y), do: 132 + def parse_param(:encoder_invert_z), do: 133 + + def parse_param(:movement_axis_nr_steps_x), do: 141 + def parse_param(:movement_axis_nr_steps_y), do: 142 + def parse_param(:movement_axis_nr_steps_z), do: 143 + + def parse_param(:movement_stop_at_max_x), do: 145 + def parse_param(:movement_stop_at_max_y), do: 146 + def parse_param(:movement_stop_at_max_z), do: 147 + + def parse_param(:pin_guard_1_pin_nr), do: 201 + def parse_param(:pin_guard_1_pin_time_out), do: 202 + def parse_param(:pin_guard_1_active_state), do: 203 + + def parse_param(:pin_guard_2_pin_nr), do: 205 + def parse_param(:pin_guard_2_pin_time_out), do: 206 + def parse_param(:pin_guard_2_active_state), do: 207 + + def parse_param(:pin_guard_3_pin_nr), do: 211 + def parse_param(:pin_guard_3_pin_time_out), do: 212 + def parse_param(:pin_guard_3_active_state), do: 213 + + def parse_param(:pin_guard_4_pin_nr), do: 215 + def parse_param(:pin_guard_4_pin_time_out), do: 216 + def parse_param(:pin_guard_4_active_state), do: 217 + + def parse_param(:pin_guard_5_pin_nr), do: 221 + def parse_param(:pin_guard_5_time_out), do: 222 + def parse_param(:pin_guard_5_active_state), do: 223 + + def parse_param(param_string) when is_bitstring(param_string), + do: param_string |> String.to_atom() |> parse_param + + def parse_param(_), do: nil +end diff --git a/lib/farmbot/firmware/gcode/parser.ex b/lib/farmbot/firmware/gcode/parser.ex index c31eda22..0c992135 100644 --- a/lib/farmbot/firmware/gcode/parser.ex +++ b/lib/farmbot/firmware/gcode/parser.ex @@ -4,6 +4,7 @@ defmodule Farmbot.Firmware.Gcode.Parser do """ require Logger + import Farmbot.Firmware.Gcode.Param @spec parse_code(binary) :: {binary, tuple} @@ -164,280 +165,4 @@ defmodule Farmbot.Firmware.Gcode.Parser do [_, rq] = String.split(q, "Q") {rq, {:report_parameter_value, parse_param(rp), String.to_integer(rv)}} end - - @doc ~S""" - Parses farmbot_arduino_firmware params. - If we want the name of param "0"\n - Example: - iex> Gcode.parse_param("0") - :param_version - - Example: - iex> Gcode.parse_param(0) - :param_version - - If we want the integer of param :param_version\n - Example: - iex> Gcode.parse_param(:param_version) - 0 - - Example: - iex> Gcode.parse_param("param_version") - 0 - """ - @spec parse_param(binary | integer) :: atom | nil - def parse_param("0"), do: :param_version - - def parse_param("2"), do: :param_config_ok - def parse_param("3"), do: :param_use_eeprom - def parse_param("4"), do: :param_e_stop_on_mov_err - def parse_param("5"), do: :param_mov_nr_retry - - def parse_param("11"), do: :movement_timeout_x - def parse_param("12"), do: :movement_timeout_y - def parse_param("13"), do: :movement_timeout_z - - def parse_param("15"), do: :movement_keep_active_x - def parse_param("16"), do: :movement_keep_active_y - def parse_param("17"), do: :movement_keep_active_z - - def parse_param("18"), do: :movement_home_at_boot_x - def parse_param("19"), do: :movement_home_at_boot_y - def parse_param("20"), do: :movement_home_at_boot_z - - def parse_param("21"), do: :movement_invert_endpoints_x - def parse_param("22"), do: :movement_invert_endpoints_y - def parse_param("23"), do: :movement_invert_endpoints_z - - def parse_param("25"), do: :movement_enable_endpoints_x - def parse_param("26"), do: :movement_enable_endpoints_y - def parse_param("27"), do: :movement_enable_endpoints_z - - def parse_param("31"), do: :movement_invert_motor_x - def parse_param("32"), do: :movement_invert_motor_y - def parse_param("33"), do: :movement_invert_motor_z - - def parse_param("36"), do: :movement_secondary_motor_x - def parse_param("37"), do: :movement_secondary_motor_invert_x - - def parse_param("41"), do: :movement_steps_acc_dec_x - def parse_param("42"), do: :movement_steps_acc_dec_y - def parse_param("43"), do: :movement_steps_acc_dec_z - - def parse_param("45"), do: :movement_stop_at_home_x - def parse_param("46"), do: :movement_stop_at_home_y - def parse_param("47"), do: :movement_stop_at_home_z - - def parse_param("51"), do: :movement_home_up_x - def parse_param("52"), do: :movement_home_up_y - def parse_param("53"), do: :movement_home_up_z - - def parse_param("55"), do: :movement_step_per_mm_x - def parse_param("56"), do: :movement_step_per_mm_y - def parse_param("57"), do: :movement_step_per_mm_z - - def parse_param("61"), do: :movement_min_spd_x - def parse_param("62"), do: :movement_min_spd_y - def parse_param("63"), do: :movement_min_spd_z - - def parse_param("65"), do: :movement_home_speed_x - def parse_param("66"), do: :movement_home_speed_y - def parse_param("67"), do: :movement_home_speed_z - - def parse_param("71"), do: :movement_max_spd_x - def parse_param("72"), do: :movement_max_spd_y - def parse_param("73"), do: :movement_max_spd_z - - def parse_param("101"), do: :encoder_enabled_x - def parse_param("102"), do: :encoder_enabled_y - def parse_param("103"), do: :encoder_enabled_z - - def parse_param("105"), do: :encoder_type_x - def parse_param("106"), do: :encoder_type_y - def parse_param("107"), do: :encoder_type_z - - def parse_param("111"), do: :encoder_missed_steps_max_x - def parse_param("112"), do: :encoder_missed_steps_max_y - def parse_param("113"), do: :encoder_missed_steps_max_z - - def parse_param("115"), do: :encoder_scaling_x - def parse_param("116"), do: :encoder_scaling_y - def parse_param("117"), do: :encoder_scaling_z - - def parse_param("121"), do: :encoder_missed_steps_decay_x - def parse_param("122"), do: :encoder_missed_steps_decay_y - def parse_param("123"), do: :encoder_missed_steps_decay_z - - def parse_param("125"), do: :encoder_use_for_pos_x - def parse_param("126"), do: :encoder_use_for_pos_y - def parse_param("127"), do: :encoder_use_for_pos_z - - def parse_param("131"), do: :encoder_invert_x - def parse_param("132"), do: :encoder_invert_y - def parse_param("133"), do: :encoder_invert_z - - def parse_param("141"), do: :movement_axis_nr_steps_x - def parse_param("142"), do: :movement_axis_nr_steps_y - def parse_param("143"), do: :movement_axis_nr_steps_z - - def parse_param("145"), do: :movement_stop_at_max_x - def parse_param("146"), do: :movement_stop_at_max_y - def parse_param("147"), do: :movement_stop_at_max_z - - def parse_param("201"), do: :pin_guard_1_pin_nr - def parse_param("202"), do: :pin_guard_1_pin_time_out - def parse_param("203"), do: :pin_guard_1_active_state - - def parse_param("205"), do: :pin_guard_2_pin_nr - def parse_param("206"), do: :pin_guard_2_pin_time_out - def parse_param("207"), do: :pin_guard_2_active_state - - def parse_param("211"), do: :pin_guard_3_pin_nr - def parse_param("212"), do: :pin_guard_3_pin_time_out - def parse_param("213"), do: :pin_guard_3_active_state - - def parse_param("215"), do: :pin_guard_4_pin_nr - def parse_param("216"), do: :pin_guard_4_pin_time_out - def parse_param("217"), do: :pin_guard_4_active_state - - def parse_param("221"), do: :pin_guard_5_pin_nr - def parse_param("222"), do: :pin_guard_5_time_out - def parse_param("223"), do: :pin_guard_5_active_state - def parse_param(param) when is_integer(param), do: parse_param("#{param}") - - @spec parse_param(atom) :: integer | nil - def parse_param(:param_version), do: 0 - - def parse_param(:param_config_ok), do: 2 - def parse_param(:param_use_eeprom), do: 3 - def parse_param(:param_e_stop_on_mov_err), do: 4 - def parse_param(:param_mov_nr_retry), do: 5 - - def parse_param(:movement_timeout_x), do: 11 - def parse_param(:movement_timeout_y), do: 12 - def parse_param(:movement_timeout_z), do: 13 - - def parse_param(:movement_keep_active_x), do: 15 - def parse_param(:movement_keep_active_y), do: 16 - def parse_param(:movement_keep_active_z), do: 17 - - def parse_param(:movement_home_at_boot_x), do: 18 - def parse_param(:movement_home_at_boot_y), do: 19 - def parse_param(:movement_home_at_boot_z), do: 20 - - def parse_param(:movement_invert_endpoints_x), do: 21 - def parse_param(:movement_invert_endpoints_y), do: 22 - def parse_param(:movement_invert_endpoints_z), do: 23 - - def parse_param(:movement_invert_motor_x), do: 31 - def parse_param(:movement_invert_motor_y), do: 32 - def parse_param(:movement_invert_motor_z), do: 33 - - def parse_param(:movement_enable_endpoints_x), do: 25 - def parse_param(:movement_enable_endpoints_y), do: 26 - def parse_param(:movement_enable_endpoints_z), do: 27 - - def parse_param(:movement_secondary_motor_x), do: 36 - def parse_param(:movement_secondary_motor_invert_x), do: 37 - - def parse_param(:movement_steps_acc_dec_x), do: 41 - def parse_param(:movement_steps_acc_dec_y), do: 42 - def parse_param(:movement_steps_acc_dec_z), do: 43 - - def parse_param(:movement_stop_at_home_x), do: 45 - def parse_param(:movement_stop_at_home_y), do: 46 - def parse_param(:movement_stop_at_home_z), do: 47 - - def parse_param(:movement_home_up_x), do: 51 - def parse_param(:movement_home_up_y), do: 52 - def parse_param(:movement_home_up_z), do: 53 - - def parse_param(:movement_step_per_mm_x), do: 55 - def parse_param(:movement_step_per_mm_y), do: 56 - def parse_param(:movement_step_per_mm_z), do: 57 - - def parse_param(:movement_min_spd_x), do: 61 - def parse_param(:movement_min_spd_y), do: 62 - def parse_param(:movement_min_spd_z), do: 63 - - def parse_param(:movement_home_speed_x), do: 65 - def parse_param(:movement_home_speed_y), do: 66 - def parse_param(:movement_home_speed_z), do: 67 - - def parse_param(:movement_max_spd_x), do: 71 - def parse_param(:movement_max_spd_y), do: 72 - def parse_param(:movement_max_spd_z), do: 73 - - def parse_param(:encoder_enabled_x), do: 101 - def parse_param(:encoder_enabled_y), do: 102 - def parse_param(:encoder_enabled_z), do: 103 - - def parse_param(:encoder_type_x), do: 105 - def parse_param(:encoder_type_y), do: 106 - def parse_param(:encoder_type_z), do: 107 - - def parse_param(:encoder_missed_steps_max_x), do: 111 - def parse_param(:encoder_missed_steps_max_y), do: 112 - def parse_param(:encoder_missed_steps_max_z), do: 113 - - def parse_param(:encoder_scaling_x), do: 115 - def parse_param(:encoder_scaling_y), do: 116 - def parse_param(:encoder_scaling_z), do: 117 - - def parse_param(:encoder_missed_steps_decay_x), do: 121 - def parse_param(:encoder_missed_steps_decay_y), do: 122 - def parse_param(:encoder_missed_steps_decay_z), do: 123 - - def parse_param(:encoder_use_for_pos_x), do: 125 - def parse_param(:encoder_use_for_pos_y), do: 126 - def parse_param(:encoder_use_for_pos_z), do: 127 - - def parse_param(:encoder_invert_x), do: 131 - def parse_param(:encoder_invert_y), do: 132 - def parse_param(:encoder_invert_z), do: 133 - - def parse_param(:movement_axis_nr_steps_x), do: 141 - def parse_param(:movement_axis_nr_steps_y), do: 142 - def parse_param(:movement_axis_nr_steps_z), do: 143 - - def parse_param(:movement_stop_at_max_x), do: 145 - def parse_param(:movement_stop_at_max_y), do: 146 - def parse_param(:movement_stop_at_max_z), do: 147 - - def parse_param(:pin_guard_1_pin_nr), do: 201 - def parse_param(:pin_guard_1_pin_time_out), do: 202 - def parse_param(:pin_guard_1_active_state), do: 203 - - def parse_param(:pin_guard_2_pin_nr), do: 205 - def parse_param(:pin_guard_2_pin_time_out), do: 206 - def parse_param(:pin_guard_2_active_state), do: 207 - - def parse_param(:pin_guard_3_pin_nr), do: 211 - def parse_param(:pin_guard_3_pin_time_out), do: 212 - def parse_param(:pin_guard_3_active_state), do: 213 - - def parse_param(:pin_guard_4_pin_nr), do: 215 - def parse_param(:pin_guard_4_pin_time_out), do: 216 - def parse_param(:pin_guard_4_active_state), do: 217 - - def parse_param(:pin_guard_5_pin_nr), do: 221 - def parse_param(:pin_guard_5_time_out), do: 222 - def parse_param(:pin_guard_5_active_state), do: 223 - - def parse_param(param_string) when is_bitstring(param_string), - do: param_string |> String.to_atom() |> parse_param - - # derp. - if Mix.env() == :dev do - def parse_param(uhh) do - Logger.error( - "Unrecognized param needs implementation " <> "#{inspect(uhh)}", - rollbar: false - ) - - nil - end - else - def parse_param(_), do: nil - end end diff --git a/lib/farmbot/firmware/handler.ex b/lib/farmbot/firmware/handler.ex new file mode 100644 index 00000000..eb6e51e4 --- /dev/null +++ b/lib/farmbot/firmware/handler.ex @@ -0,0 +1,57 @@ +defmodule Farmbot.Firmware.Handler do + @moduledoc """ + Any module that implements this behaviour should be a GenStage. + + The implementng stage should communicate with the various Farmbot + hardware such as motors and encoders. The `Farmbot.Firmware` module + will subscribe_to: the implementing handler. Events should be + Gcodes as parsed by `Farmbot.Firmware.Gcode.Parser`. + """ + + @doc "Start a firmware handler." + @callback start_link :: GenServer.on_start() + + @typedoc false + @type fw_ret_val :: :ok | {:error, term} + + @typedoc false + @type vec3 :: Farmbot.Firmware.Vec3.t + + @typedoc false + @type axis :: Farmbot.Firmware.Vec3.axis + + @typedoc false + @type fw_param :: Farmbot.Firmware.Gcode.Param.t + + @typedoc "Pin" + @type pin :: number + + @typedoc "Mode of a pin." + @type pin_mode :: :digital | :analog + + @doc "Move to a position." + @callback move_absolute(vec3) :: fw_ret_val + + @doc "Calibrate an axis." + @callback calibrate(axis) :: fw_ret_val + + @doc "Update a paramater." + @callback update_param(fw_param, number) :: fw_ret_val + + @callback read_param(fw_param) :: {:ok, number} | {:error, term} + + @doc "Lock the firmware." + @callback emergency_lock() :: fw_ret_val + + @doc "Unlock the firmware." + @callback emergency_unlock() :: fw_ret_val + + @doc "Find home on an axis." + @callback find_home(axis) :: fw_ret_val + + @doc "Read a pin." + @callback read_pin(pin, pin_mode) :: {:ok, number} | {:error, term} + + @doc "Write a pin." + @callback write_pin(pin, pin_mode, number) :: fw_ret_val +end diff --git a/lib/farmbot/firmware/uart_handler.ex b/lib/farmbot/firmware/uart_handler/uart_handler.ex similarity index 100% rename from lib/farmbot/firmware/uart_handler.ex rename to lib/farmbot/firmware/uart_handler/uart_handler.ex diff --git a/lib/farmbot/firmware/vec3.ex b/lib/farmbot/firmware/vec3.ex new file mode 100644 index 00000000..4deb4d8e --- /dev/null +++ b/lib/farmbot/firmware/vec3.ex @@ -0,0 +1,11 @@ +defmodule Farmbot.Firmware.Vec3 do + @moduledoc "A three position vector." + + defstruct [:x, :y, :z] + + @typedoc "Axis label." + @type axis :: :x | :y | :z + + @typedoc @moduledoc + @type t :: %__MODULE__{x: number, y: number, z: number} +end diff --git a/lib/farmbot/http/adapter.ex b/lib/farmbot/http/adapter.ex new file mode 100644 index 00000000..309c5cee --- /dev/null +++ b/lib/farmbot/http/adapter.ex @@ -0,0 +1,2 @@ +defmodule Farmbot.HTTP.Adapter do +end diff --git a/lib/farmbot/http/http.ex b/lib/farmbot/http/http.ex index ef45b4fd..9b52bc47 100644 --- a/lib/farmbot/http/http.ex +++ b/lib/farmbot/http/http.ex @@ -1,491 +1,26 @@ defmodule Farmbot.HTTP do - @moduledoc """ - Farmbot HTTP adapter for accessing the world and farmbot web api easily. - """ + @moduledoc "Wraps an HTTP Adapter." use GenServer - alias HTTPoison - alias HTTPoison.{AsyncResponse, AsyncStatus, AsyncHeaders, AsyncChunk, AsyncEnd} - alias Farmbot.HTTP.{Response, Helpers, Error} - import Helpers - require Logger - @version Mix.Project.config()[:version] - @target Mix.Project.config()[:target] - @redirect_status_codes [301, 302, 303, 307, 308] + @adapter Application.get_env(:farmbot, :behaviour)[:http_adapter] || raise("No http adapter.") - @doc """ - Make an http request. Will not raise. - * `method` - can be any http verb - * `url` - fully formatted url or an api slug. - * `body` - body can be any of: - * binary - * `{:multipart, [{binary_key, binary_value}]}` - * headers - `[{binary_key, binary_value}]` - * opts - Keyword opts to be passed to adapter (hackney/httpoison) - * `file` - option to be passed if the output should be saved to a file. - """ - def request(http, method, url, body \\ "", headers \\ [], opts \\ []) + @doc "Make an HTTP Request." + def request(method, url, body \\ "", headers \\ [], opts \\ []) - def request(http, method, url, body, headers, opts) do - GenServer.call(http, {:req, method, url, body, headers, opts}, :infinity) + @doc "HTTP GET request." + def get(url, headers \\ [], opts \\ []) + + @doc "HTTP POST request." + def post(url, headers \\ [], opts \\ []) + + @doc "Start HTTP Services." + def start_link do + GenServer.start_link(__MODULE__, [], name: __MODULE__) end - @doc """ - Same as `request/6` - but raises a `Farmbot.HTTP.Error` exception. - """ - def request!(http, method, url, body \\ "", headers \\ [], opts \\ []) - - def request!(http, method, url, body, headers, opts) do - case request(http, method, url, body, headers, opts) do - {:ok, %Response{status_code: code} = resp} when is_2xx(code) -> resp - {:ok, %Response{} = resp} -> raise Error, resp - {:error, error} when is_binary(error) or is_atom(error) -> raise Error, "#{error}" - {:error, error} -> raise Error, inspect(error) - end - end - - methods = [:get, :post, :delete, :patch, :put] - - for verb <- methods do - @doc """ - HTTP #{verb} request. - """ - def unquote(verb)(http, url, body \\ "", headers \\ [], opts \\ []) - - def unquote(verb)(http, url, body, headers, opts) do - request(http, unquote(verb), url, body, headers, opts) - end - - @doc """ - Same as #{verb}/5 but raises Farmbot.HTTP.Error exception. - """ - fun_name = "#{verb}!" |> String.to_atom() - def unquote(fun_name)(http, url, body \\ "", headers \\ [], opts \\ []) - - def unquote(fun_name)(http, url, body, headers, opts) do - request!(http, unquote(verb), url, body, headers, opts) - end - end - - def download_file(http, url, path, progress_callback \\ nil, payload \\ "", headers \\ []) - - def download_file(http, url, path, progress_callback, payload, headers) do - case get(http, url, payload, headers, file: path, progress_callback: progress_callback) do - {:ok, %Response{status_code: code}} when is_2xx(code) -> {:ok, path} - {:ok, %Response{} = resp} -> {:error, resp} - {:error, reason} -> {:error, reason} - end - end - - def download_file!(http, url, path, progress_callback \\ nil, payload \\ "", headers \\ []) - - def download_file!(http, url, path, progress_callback, payload, headers) do - case download_file(http, url, path, progress_callback, payload, headers) do - {:ok, path} -> path - {:error, reason} -> raise Error, reason - end - end - - def upload_file(http, path, meta \\ nil) do - if File.exists?(path) do - http - |> get("/api/storage_auth") - |> do_multipart_request(http, meta || %{x: -1, y: -1, z: -1}, path) - else - {:error, "#{path} not found"} - end - end - - def upload_file!(http, path, meta \\ %{}) do - case upload_file(http, path, meta) do - :ok -> :ok - {:error, reason} -> raise Error, reason - end - end - - defp do_multipart_request({:ok, %Response{status_code: code, body: bin_body}}, http, meta, path) - when is_2xx(code) do - with {:ok, body} <- Poison.decode(bin_body), - {:ok, file} <- File.read(path) do - url = "https:" <> body["url"] - form_data = body["form_data"] - attachment_url = url <> form_data["key"] - - mp = - Enum.map(form_data, fn {key, val} -> - if key == "file", do: {"file", file}, else: {key, val} - end) - - http - |> post(url, {:multipart, mp}) - |> finish_upload(http, attachment_url, meta) - end - end - - defp do_multipart_request({:ok, %Response{} = response}, _http, _meta, _path), - do: {:error, response} - - defp do_multipart_request({:error, reason}, _http, _meta, _path), do: {:error, reason} - - defp finish_upload({:ok, %Response{status_code: code}}, http, atch_url, meta) when is_2xx(code) do - with {:ok, body} <- Poison.encode(%{"attachment_url" => atch_url, "meta" => meta}) do - case post(http, "/api/images", body) do - {:ok, %Response{status_code: code}} when is_2xx(code) -> - # debug_log("#{atch_url} should exist shortly.") - :ok - - {:ok, %Response{} = response} -> - {:error, response} - - {:error, reason} -> - {:error, reason} - end - end - end - - defp finish_upload({:ok, %Response{} = resp}, _http, _url, _meta), do: {:error, resp} - defp finish_upload({:error, reason}, _http, _url, _meta), do: {:error, reason} - - # GenServer - - defmodule State do - defstruct [:token, :requests] - - defimpl Inspect, for: __MODULE__ do - def inspect(_state, _), do: "#HTTPState<>" - end - end - - defmodule Buffer do - defstruct [ - :data, - :headers, - :status_code, - :request, - :from, - :id, - :file, - :timeout, - :progress_callback, - :file_size - ] - end - - def start_link(token, opts) do - GenServer.start_link(__MODULE__, token, opts) - end - - def init(token) do - state = %State{token: token, requests: %{}} - {:ok, state} - end - - def handle_call({:req, method, url, body, headers, opts}, from, state) do - {file, opts} = maybe_open_file(opts) - opts = fb_opts(opts) - headers = fb_headers(headers) - # debug_log "#{inspect Tuple.delete_at(from, 0)} Request start (#{url})" - # Pattern match the url. - case url do - "/api" <> _ -> do_api_request({method, url, body, headers, opts, from}, state) - _ -> do_normal_request({method, url, body, headers, opts, from}, file, state) - end - end - - def handle_info({:timeout, ref}, state) do - case state.requests[ref] do - %Buffer{} = buffer -> - GenServer.reply(buffer.from, {:error, :timeout}) - {:noreply, %{state | requests: Map.delete(state.requests, ref)}} - - nil -> - {:noreply, state} - end - end - - def handle_info(%AsyncStatus{code: code, id: ref}, state) do - case state.requests[ref] do - %Buffer{} = buffer -> - # debug_log "#{inspect Tuple.delete_at(buffer.from, 0)} Got Status." - HTTPoison.stream_next(%AsyncResponse{id: ref}) - {:noreply, %{state | requests: %{state.requests | ref => %{buffer | status_code: code}}}} - - nil -> - {:noreply, state} - end - end - - def handle_info(%AsyncHeaders{headers: headers, id: ref}, state) do - case state.requests[ref] do - %Buffer{} = buffer -> - # debug_log("#{inspect Tuple.delete_at(buffer.from, 0)} Got headers") - file_size = - Enum.find_value(headers, fn {header, val} -> - case header do - "Content-Length" -> val - "content_length" -> val - _header -> nil - end - end) - - HTTPoison.stream_next(%AsyncResponse{id: ref}) - - { - :noreply, - %{ - state - | requests: %{ - state.requests - | ref => %{buffer | headers: headers, file_size: file_size} - } - } - } - - nil -> - {:noreply, state} - end - end - - def handle_info(%AsyncChunk{chunk: chunk, id: ref}, state) do - case state.requests[ref] do - %Buffer{} = buffer -> - if buffer.timeout, do: Process.cancel_timer(buffer.timeout) - timeout = Process.send_after(self(), {:timeout, ref}, 30000) - maybe_log_progress(buffer) - maybe_stream_to_file(buffer.file, buffer.status_code, chunk) - HTTPoison.stream_next(%AsyncResponse{id: ref}) - - { - :noreply, - %{ - state - | requests: %{ - state.requests - | ref => %{buffer | data: buffer.data <> chunk, timeout: timeout} - } - } - } - - nil -> - {:noreply, state} - end - end - - def handle_info(%AsyncEnd{id: ref}, state) do - case state.requests[ref] do - %Buffer{} = buffer -> - # debug_log "#{inspect Tuple.delete_at(buffer.from, 0)} Request finish." - finish_request(buffer, state) - - nil -> - {:noreply, state} - end - end - - def terminate({:error, reason}, state), do: terminate(reason, state) - - def terminate(reason, state) do - for {_ref, buffer} <- state.requests do - maybe_close_file(buffer.file) - GenServer.reply(buffer.from, {:error, reason}) - end - end - - defp maybe_open_file(opts) do - {file, opts} = Keyword.pop(opts, :file) - - case file do - filename when is_binary(filename) -> - # debug_log "Opening file: #{filename}" - File.rm(file) - :ok = File.touch(filename) - {:ok, fd} = :file.open(filename, [:write, :raw]) - {fd, Keyword.merge(opts, stream_to: self(), async: :once)} - - _ -> - {nil, opts} - end - end - - defp maybe_stream_to_file(nil, _, _data), do: :ok - defp maybe_stream_to_file(_, code, _data) when code in @redirect_status_codes, do: :ok - - defp maybe_stream_to_file(fd, _code, data) when is_binary(data) do - # debug_log "[#{inspect self()}] writing data to file." - :ok = :file.write(fd, data) - end - - defp maybe_close_file(nil), do: :ok - defp maybe_close_file(fd), do: :file.close(fd) - - defp maybe_log_progress(%Buffer{file: file, progress_callback: pcb}) - when is_nil(file) or is_nil(pcb) do - # debug_log "File (#{inspect file}) or progress callback: #{inspect pcb} are nil" - :ok - end - - defp maybe_log_progress(%Buffer{file: _file, file_size: fs} = buffer) do - downloaded_bytes = byte_size(buffer.data) - - case fs do - numstr when is_binary(numstr) -> - total_bytes = numstr |> String.to_integer() - buffer.progress_callback.(downloaded_bytes, total_bytes) - - other when other in [:complete] or is_nil(other) -> - buffer.progress_callback.(downloaded_bytes, other) - end - end - - defp do_api_request({method, url, body, headers, opts, from}, %{token: tkn} = state) do - headers = - headers - |> add_header({"Authorization", "Bearer " <> tkn}) - |> add_header({"Content-Type", "application/json"}) - - opts = opts |> Keyword.put(:timeout, :infinity) - # TODO Fix this. - url = "https:" <> Farmbot.Jwt.decode!(tkn).iss <> url - do_normal_request({method, url, body, headers, opts, from}, nil, state) - end - - defp do_normal_request({method, url, body, headers, opts, from}, file, state) do - case HTTPoison.request(method, url, body, headers, opts) do - {:ok, %HTTPoison.Response{status_code: code, headers: resp_headers}} - when code in @redirect_status_codes -> - redir = - Enum.find_value(resp_headers, fn {header, val} -> - if header == "Location", do: val, else: false - end) - - if redir do - # debug_log "redirect" - do_normal_request({method, redir, body, headers, opts, from}, file, state) - else - # debug_log("Failed to redirect: #{inspect resp}") - GenServer.reply(from, {:error, :no_server_for_redirect}) - {:noreply, state} - end - - {:ok, %HTTPoison.Response{body: body, headers: headers, status_code: code}} -> - GenServer.reply(from, {:ok, %Response{body: body, headers: headers, status_code: code}}) - {:noreply, state} - - {:ok, %AsyncResponse{id: ref}} -> - timeout = Process.send_after(self(), {:timeout, ref}, 30000) - - req = %Buffer{ - id: ref, - from: from, - timeout: timeout, - file: file, - data: "", - headers: nil, - status_code: nil, - progress_callback: Keyword.fetch!(opts, :progress_callback), - request: {method, url, body, headers, opts} - } - - {:noreply, %{state | requests: Map.put(state.requests, ref, req)}} - - {:error, %HTTPoison.Error{reason: reason}} -> - GenServer.reply(from, {:error, reason}) - {:noreply, state} - - {:error, reason} -> - GenServer.reply(from, {:error, reason}) - {:noreply, state} - end - end - - defp do_redirect_request(%Buffer{} = buffer, redir, state) do - {method, _url, body, headers, opts} = buffer.request - - case HTTPoison.request(method, redir, body, headers, opts) do - {:ok, %AsyncResponse{id: ref}} -> - req = %Buffer{ - buffer - | id: ref, - from: buffer.from, - file: buffer.file, - data: "", - headers: nil, - status_code: nil, - request: {method, redir, body, headers, opts} - } - - state = %{state | requests: Map.delete(state.requests, buffer.id)} - state = %{state | requests: Map.put(state.requests, ref, req)} - {:noreply, state} - - {:error, %HTTPoison.Error{reason: reason}} -> - GenServer.reply(buffer.from, {:error, reason}) - {:noreply, state} - - {:error, reason} -> - GenServer.reply(buffer.from, {:error, reason}) - {:noreply, state} - end - end - - defp finish_request(%Buffer{status_code: status_code} = buffer, state) - when status_code in @redirect_status_codes do - # debug_log "#{inspect Tuple.delete_at(buffer.from, 0)} Trying to redirect: (#{status_code})" - redir = - Enum.find_value(buffer.headers, fn {header, val} -> - case header do - "Location" -> val - "location" -> val - _ -> false - end - end) - - if redir do - do_redirect_request(buffer, redir, state) - else - # debug_log("Failed to redirect: #{inspect buffer}") - GenServer.reply(buffer.from, {:error, :no_server_for_redirect}) - {:noreply, state} - end - end - - defp finish_request(%Buffer{} = buffer, state) do - # debug_log "Request finish." - response = %Response{ - status_code: buffer.status_code, - body: buffer.data, - headers: buffer.headers - } - - if buffer.timeout, do: Process.cancel_timer(buffer.timeout) - maybe_close_file(buffer.file) - - case buffer.file_size do - nil -> maybe_log_progress(%{buffer | file_size: :complete}) - _num -> maybe_log_progress(%{buffer | file_size: "#{byte_size(buffer.data)}"}) - end - - GenServer.reply(buffer.from, {:ok, response}) - {:noreply, %{state | requests: Map.delete(state.requests, buffer.id)}} - end - - defp fb_headers(headers) do - headers |> add_header({"User-Agent", "FarmbotOS/#{@version} (#{@target}) #{@target} ()"}) - end - - defp add_header(headers, new), do: [new | headers] - - defp fb_opts(opts) do - Keyword.merge( - opts, - ssl: [{:versions, [:"tlsv1.2"]}], - hackney: [:insecure, pool: :farmbot_http_pool], - recv_timeout: :infinity, - timeout: :infinity - ) - - # stream_to: self(), - # follow_redirect: false, - # async: :once + def init([]) do + {:ok, adapter} = @adapter.start_link() + Process.link(adapter) + {:ok, %{adapter: adapter}} end end diff --git a/lib/farmbot/http/httpoison_adapter.ex b/lib/farmbot/http/httpoison_adapter.ex new file mode 100644 index 00000000..dd16b57c --- /dev/null +++ b/lib/farmbot/http/httpoison_adapter.ex @@ -0,0 +1,493 @@ +defmodule Farmbot.HTTP.HTTPoisonAdapter do + @moduledoc """ + Farmbot HTTP adapter for accessing the world and farmbot web api easily. + """ + + use GenServer + alias HTTPoison + alias HTTPoison.{AsyncResponse, AsyncStatus, AsyncHeaders, AsyncChunk, AsyncEnd} + alias Farmbot.HTTP.{Response, Helpers, Error} + import Helpers + require Logger + + @behaviour Farmbot.HTTP.Adapter + + @version Mix.Project.config()[:version] + @target Mix.Project.config()[:target] + @redirect_status_codes [301, 302, 303, 307, 308] + + @doc """ + Make an http request. Will not raise. + * `method` - can be any http verb + * `url` - fully formatted url or an api slug. + * `body` - body can be any of: + * binary + * `{:multipart, [{binary_key, binary_value}]}` + * headers - `[{binary_key, binary_value}]` + * opts - Keyword opts to be passed to adapter (hackney/httpoison) + * `file` - option to be passed if the output should be saved to a file. + """ + def request(http, method, url, body \\ "", headers \\ [], opts \\ []) + + def request(http, method, url, body, headers, opts) do + GenServer.call(http, {:req, method, url, body, headers, opts}, :infinity) + end + + @doc """ + Same as `request/6` - but raises a `Farmbot.HTTP.Error` exception. + """ + def request!(http, method, url, body \\ "", headers \\ [], opts \\ []) + + def request!(http, method, url, body, headers, opts) do + case request(http, method, url, body, headers, opts) do + {:ok, %Response{status_code: code} = resp} when is_2xx(code) -> resp + {:ok, %Response{} = resp} -> raise Error, resp + {:error, error} when is_binary(error) or is_atom(error) -> raise Error, "#{error}" + {:error, error} -> raise Error, inspect(error) + end + end + + methods = [:get, :post, :delete, :patch, :put] + + for verb <- methods do + @doc """ + HTTP #{verb} request. + """ + def unquote(verb)(http, url, body \\ "", headers \\ [], opts \\ []) + + def unquote(verb)(http, url, body, headers, opts) do + request(http, unquote(verb), url, body, headers, opts) + end + + @doc """ + Same as #{verb}/5 but raises Farmbot.HTTP.Error exception. + """ + fun_name = "#{verb}!" |> String.to_atom() + def unquote(fun_name)(http, url, body \\ "", headers \\ [], opts \\ []) + + def unquote(fun_name)(http, url, body, headers, opts) do + request!(http, unquote(verb), url, body, headers, opts) + end + end + + def download_file(http, url, path, progress_callback \\ nil, payload \\ "", headers \\ []) + + def download_file(http, url, path, progress_callback, payload, headers) do + case get(http, url, payload, headers, file: path, progress_callback: progress_callback) do + {:ok, %Response{status_code: code}} when is_2xx(code) -> {:ok, path} + {:ok, %Response{} = resp} -> {:error, resp} + {:error, reason} -> {:error, reason} + end + end + + def download_file!(http, url, path, progress_callback \\ nil, payload \\ "", headers \\ []) + + def download_file!(http, url, path, progress_callback, payload, headers) do + case download_file(http, url, path, progress_callback, payload, headers) do + {:ok, path} -> path + {:error, reason} -> raise Error, reason + end + end + + def upload_file(http, path, meta \\ nil) do + if File.exists?(path) do + http + |> get("/api/storage_auth") + |> do_multipart_request(http, meta || %{x: -1, y: -1, z: -1}, path) + else + {:error, "#{path} not found"} + end + end + + def upload_file!(http, path, meta \\ %{}) do + case upload_file(http, path, meta) do + :ok -> :ok + {:error, reason} -> raise Error, reason + end + end + + defp do_multipart_request({:ok, %Response{status_code: code, body: bin_body}}, http, meta, path) + when is_2xx(code) do + with {:ok, body} <- Poison.decode(bin_body), + {:ok, file} <- File.read(path) do + url = "https:" <> body["url"] + form_data = body["form_data"] + attachment_url = url <> form_data["key"] + + mp = + Enum.map(form_data, fn {key, val} -> + if key == "file", do: {"file", file}, else: {key, val} + end) + + http + |> post(url, {:multipart, mp}) + |> finish_upload(http, attachment_url, meta) + end + end + + defp do_multipart_request({:ok, %Response{} = response}, _http, _meta, _path), + do: {:error, response} + + defp do_multipart_request({:error, reason}, _http, _meta, _path), do: {:error, reason} + + defp finish_upload({:ok, %Response{status_code: code}}, http, atch_url, meta) when is_2xx(code) do + with {:ok, body} <- Poison.encode(%{"attachment_url" => atch_url, "meta" => meta}) do + case post(http, "/api/images", body) do + {:ok, %Response{status_code: code}} when is_2xx(code) -> + # debug_log("#{atch_url} should exist shortly.") + :ok + + {:ok, %Response{} = response} -> + {:error, response} + + {:error, reason} -> + {:error, reason} + end + end + end + + defp finish_upload({:ok, %Response{} = resp}, _http, _url, _meta), do: {:error, resp} + defp finish_upload({:error, reason}, _http, _url, _meta), do: {:error, reason} + + # GenServer + + defmodule State do + defstruct [:requests] + + end + + defmodule Buffer do + defstruct [ + :data, + :headers, + :status_code, + :request, + :from, + :id, + :file, + :timeout, + :progress_callback, + :file_size + ] + end + + def start_link() do + GenServer.start_link(__MODULE__, [], []) + end + + def init(token) do + state = %State{requests: %{}} + {:ok, state} + end + + def handle_call({:req, method, url, body, headers, opts}, from, state) do + {file, opts} = maybe_open_file(opts) + opts = fb_opts(opts) + headers = fb_headers(headers) + # debug_log "#{inspect Tuple.delete_at(from, 0)} Request start (#{url})" + # Pattern match the url. + case url do + "/api" <> _ -> do_api_request({method, url, body, headers, opts, from}, state) + _ -> do_normal_request({method, url, body, headers, opts, from}, file, state) + end + end + + def handle_info({:timeout, ref}, state) do + case state.requests[ref] do + %Buffer{} = buffer -> + GenServer.reply(buffer.from, {:error, :timeout}) + {:noreply, %{state | requests: Map.delete(state.requests, ref)}} + + nil -> + {:noreply, state} + end + end + + def handle_info(%AsyncStatus{code: code, id: ref}, state) do + case state.requests[ref] do + %Buffer{} = buffer -> + # debug_log "#{inspect Tuple.delete_at(buffer.from, 0)} Got Status." + HTTPoison.stream_next(%AsyncResponse{id: ref}) + {:noreply, %{state | requests: %{state.requests | ref => %{buffer | status_code: code}}}} + + nil -> + {:noreply, state} + end + end + + def handle_info(%AsyncHeaders{headers: headers, id: ref}, state) do + case state.requests[ref] do + %Buffer{} = buffer -> + # debug_log("#{inspect Tuple.delete_at(buffer.from, 0)} Got headers") + file_size = + Enum.find_value(headers, fn {header, val} -> + case header do + "Content-Length" -> val + "content_length" -> val + _header -> nil + end + end) + + HTTPoison.stream_next(%AsyncResponse{id: ref}) + + { + :noreply, + %{ + state + | requests: %{ + state.requests + | ref => %{buffer | headers: headers, file_size: file_size} + } + } + } + + nil -> + {:noreply, state} + end + end + + def handle_info(%AsyncChunk{chunk: chunk, id: ref}, state) do + case state.requests[ref] do + %Buffer{} = buffer -> + if buffer.timeout, do: Process.cancel_timer(buffer.timeout) + timeout = Process.send_after(self(), {:timeout, ref}, 30000) + maybe_log_progress(buffer) + maybe_stream_to_file(buffer.file, buffer.status_code, chunk) + HTTPoison.stream_next(%AsyncResponse{id: ref}) + + { + :noreply, + %{ + state + | requests: %{ + state.requests + | ref => %{buffer | data: buffer.data <> chunk, timeout: timeout} + } + } + } + + nil -> + {:noreply, state} + end + end + + def handle_info(%AsyncEnd{id: ref}, state) do + case state.requests[ref] do + %Buffer{} = buffer -> + # debug_log "#{inspect Tuple.delete_at(buffer.from, 0)} Request finish." + finish_request(buffer, state) + + nil -> + {:noreply, state} + end + end + + def terminate({:error, reason}, state), do: terminate(reason, state) + + def terminate(reason, state) do + for {_ref, buffer} <- state.requests do + maybe_close_file(buffer.file) + GenServer.reply(buffer.from, {:error, reason}) + end + end + + defp maybe_open_file(opts) do + {file, opts} = Keyword.pop(opts, :file) + + case file do + filename when is_binary(filename) -> + # debug_log "Opening file: #{filename}" + File.rm(file) + :ok = File.touch(filename) + {:ok, fd} = :file.open(filename, [:write, :raw]) + {fd, Keyword.merge(opts, stream_to: self(), async: :once)} + + _ -> + {nil, opts} + end + end + + defp maybe_stream_to_file(nil, _, _data), do: :ok + defp maybe_stream_to_file(_, code, _data) when code in @redirect_status_codes, do: :ok + + defp maybe_stream_to_file(fd, _code, data) when is_binary(data) do + # debug_log "[#{inspect self()}] writing data to file." + :ok = :file.write(fd, data) + end + + defp maybe_close_file(nil), do: :ok + defp maybe_close_file(fd), do: :file.close(fd) + + defp maybe_log_progress(%Buffer{file: file, progress_callback: pcb}) + when is_nil(file) or is_nil(pcb) do + # debug_log "File (#{inspect file}) or progress callback: #{inspect pcb} are nil" + :ok + end + + defp maybe_log_progress(%Buffer{file: _file, file_size: fs} = buffer) do + downloaded_bytes = byte_size(buffer.data) + + case fs do + numstr when is_binary(numstr) -> + total_bytes = numstr |> String.to_integer() + buffer.progress_callback.(downloaded_bytes, total_bytes) + + other when other in [:complete] or is_nil(other) -> + buffer.progress_callback.(downloaded_bytes, other) + end + end + + defp do_api_request({method, url, body, headers, opts, from}, state) do + #TODO Get token and url out of config storage. + token = "" + url = "" + + headers = + headers + |> add_header({"Authorization", "Bearer " <> tkn}) + |> add_header({"Content-Type", "application/json"}) + + opts = opts |> Keyword.put(:timeout, :infinity) + do_normal_request({method, url, body, headers, opts, from}, nil, state) + end + + defp do_normal_request({method, url, body, headers, opts, from}, file, state) do + case HTTPoison.request(method, url, body, headers, opts) do + {:ok, %HTTPoison.Response{status_code: code, headers: resp_headers}} + when code in @redirect_status_codes -> + redir = + Enum.find_value(resp_headers, fn {header, val} -> + if header == "Location", do: val, else: false + end) + + if redir do + # debug_log "redirect" + do_normal_request({method, redir, body, headers, opts, from}, file, state) + else + # debug_log("Failed to redirect: #{inspect resp}") + GenServer.reply(from, {:error, :no_server_for_redirect}) + {:noreply, state} + end + + {:ok, %HTTPoison.Response{body: body, headers: headers, status_code: code}} -> + GenServer.reply(from, {:ok, %Response{body: body, headers: headers, status_code: code}}) + {:noreply, state} + + {:ok, %AsyncResponse{id: ref}} -> + timeout = Process.send_after(self(), {:timeout, ref}, 30000) + + req = %Buffer{ + id: ref, + from: from, + timeout: timeout, + file: file, + data: "", + headers: nil, + status_code: nil, + progress_callback: Keyword.fetch!(opts, :progress_callback), + request: {method, url, body, headers, opts} + } + + {:noreply, %{state | requests: Map.put(state.requests, ref, req)}} + + {:error, %HTTPoison.Error{reason: reason}} -> + GenServer.reply(from, {:error, reason}) + {:noreply, state} + + {:error, reason} -> + GenServer.reply(from, {:error, reason}) + {:noreply, state} + end + end + + defp do_redirect_request(%Buffer{} = buffer, redir, state) do + {method, _url, body, headers, opts} = buffer.request + + case HTTPoison.request(method, redir, body, headers, opts) do + {:ok, %AsyncResponse{id: ref}} -> + req = %Buffer{ + buffer + | id: ref, + from: buffer.from, + file: buffer.file, + data: "", + headers: nil, + status_code: nil, + request: {method, redir, body, headers, opts} + } + + state = %{state | requests: Map.delete(state.requests, buffer.id)} + state = %{state | requests: Map.put(state.requests, ref, req)} + {:noreply, state} + + {:error, %HTTPoison.Error{reason: reason}} -> + GenServer.reply(buffer.from, {:error, reason}) + {:noreply, state} + + {:error, reason} -> + GenServer.reply(buffer.from, {:error, reason}) + {:noreply, state} + end + end + + defp finish_request(%Buffer{status_code: status_code} = buffer, state) + when status_code in @redirect_status_codes do + # debug_log "#{inspect Tuple.delete_at(buffer.from, 0)} Trying to redirect: (#{status_code})" + redir = + Enum.find_value(buffer.headers, fn {header, val} -> + case header do + "Location" -> val + "location" -> val + _ -> false + end + end) + + if redir do + do_redirect_request(buffer, redir, state) + else + # debug_log("Failed to redirect: #{inspect buffer}") + GenServer.reply(buffer.from, {:error, :no_server_for_redirect}) + {:noreply, state} + end + end + + defp finish_request(%Buffer{} = buffer, state) do + # debug_log "Request finish." + response = %Response{ + status_code: buffer.status_code, + body: buffer.data, + headers: buffer.headers + } + + if buffer.timeout, do: Process.cancel_timer(buffer.timeout) + maybe_close_file(buffer.file) + + case buffer.file_size do + nil -> maybe_log_progress(%{buffer | file_size: :complete}) + _num -> maybe_log_progress(%{buffer | file_size: "#{byte_size(buffer.data)}"}) + end + + GenServer.reply(buffer.from, {:ok, response}) + {:noreply, %{state | requests: Map.delete(state.requests, buffer.id)}} + end + + defp fb_headers(headers) do + headers |> add_header({"User-Agent", "FarmbotOS/#{@version} (#{@target}) #{@target} ()"}) + end + + defp add_header(headers, new), do: [new | headers] + + defp fb_opts(opts) do + Keyword.merge( + opts, + ssl: [{:versions, [:"tlsv1.2"]}], + hackney: [:insecure, pool: :farmbot_http_pool], + recv_timeout: :infinity, + timeout: :infinity + ) + + # stream_to: self(), + # follow_redirect: false, + # async: :once + end +end diff --git a/lib/farmbot/http/supervisor.ex b/lib/farmbot/http/supervisor.ex index 8a21d246..d555d473 100644 --- a/lib/farmbot/http/supervisor.ex +++ b/lib/farmbot/http/supervisor.ex @@ -10,7 +10,7 @@ defmodule Farmbot.HTTP.Supervisor do def init(token) do children = [ - worker(Farmbot.HTTP, [token, [name: Farmbot.HTTP]]) + worker(Farmbot.HTTP) ] opts = [strategy: :one_for_all]