[BROKEN] begin allowing http adapter to be replaced.

pull/363/head
Connor Rigby 2017-10-23 19:14:30 -07:00
parent 9ecbb78f11
commit f78ce3773c
15 changed files with 865 additions and 790 deletions

1
.gitignore vendored
View File

@ -54,3 +54,4 @@ tmp
*.sqlite3-wal
*.img
run-qemu.sh
auth_secret.exs

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,2 @@
defmodule Farmbot.HTTP.Adapter do
end

View File

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

View File

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

View File

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