prepare for merge
commit
345287a534
|
@ -11,7 +11,7 @@ config :ssl, protocol_version: :"tlsv1.2"
|
|||
# I force colors because they are important.
|
||||
config :logger, :console,
|
||||
colors: [enabled: true, info: :cyan],
|
||||
metadata: [:module],
|
||||
metadata: [],
|
||||
format: "$time $metadata[$level] $levelpad$message\n"
|
||||
|
||||
# Iex needs colors too.
|
||||
|
|
|
@ -15,7 +15,7 @@ config :farmbot, :init, [
|
|||
|
||||
# Transports.
|
||||
config :farmbot, :transport, [
|
||||
Farmbot.BotState.Transport.GenMqtt
|
||||
Farmbot.BotState.Transport.GenMQTT
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -1,134 +1,36 @@
|
|||
defmodule Farmbot.BotState do
|
||||
@moduledoc """
|
||||
State tree of the bot.
|
||||
"""
|
||||
alias Farmbot.BotState.{
|
||||
Configuration,
|
||||
InformationalSettings,
|
||||
Job,
|
||||
LocationData,
|
||||
McuParams,
|
||||
Pin,
|
||||
ProcessInfo
|
||||
}
|
||||
|
||||
defstruct [
|
||||
configuration: %Configuration{},
|
||||
informational_settings: %InformationalSettings{},
|
||||
location_data: %LocationData{},
|
||||
mcu_params: %McuParams{},
|
||||
process_info: %ProcessInfo{},
|
||||
jobs: %{},
|
||||
pins: %{},
|
||||
user_env: %{},
|
||||
]
|
||||
|
||||
@typedoc "Bot State"
|
||||
@type t :: %__MODULE__{
|
||||
informational_settings: InformationalSettings.t,
|
||||
configuration: Configuration.t,
|
||||
location_data: LocationData.t,
|
||||
process_info: ProcessInfo.t,
|
||||
mcu_params: McuParams.t,
|
||||
jobs: %{optional(binary) => Job.t},
|
||||
pins: %{optional(number) => Pin.t},
|
||||
user_env: %{optional(binary) => binary}
|
||||
}
|
||||
|
||||
@typedoc "Instance of this module."
|
||||
@type state_server :: GenServer.server
|
||||
|
||||
@doc """
|
||||
Subscribe to updates from the bot.
|
||||
|
||||
## Updates
|
||||
Updates will come in randomly in the shape of
|
||||
`{:bot_state, state}` where state is a BotState struct.
|
||||
"""
|
||||
@spec subscribe(state_server) :: :ok | {:error, :already_subscribed}
|
||||
def subscribe(state_tracker) do
|
||||
GenServer.call(state_tracker, :subscribe)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unsubscribes from the bot state tracker.
|
||||
returns a boolean where true means the called pid was
|
||||
actually subscribed and false means it was not actually subscribed.
|
||||
"""
|
||||
@spec unsubscribe(state_server) :: boolean
|
||||
def unsubscribe(state_tracker) do
|
||||
GenServer.call(state_tracker, :unsubscribe)
|
||||
end
|
||||
|
||||
@doc "Forces a dispatch to all the clients."
|
||||
@spec force_dispatch(state_server) :: :ok
|
||||
def force_dispatch(state_server) do
|
||||
GenServer.call(state_server, :force_dispatch)
|
||||
end
|
||||
|
||||
## GenServer
|
||||
|
||||
use GenServer
|
||||
use GenStage
|
||||
require Logger
|
||||
|
||||
# these modules have seperate process backing them.
|
||||
@update_mods [InformationalSettings, Configuration, LocationData, ProcessInfo, McuParams]
|
||||
defstruct [
|
||||
mcu_params: %{},
|
||||
jobs: %{},
|
||||
location_data: %{},
|
||||
pins: %{},
|
||||
configuration: %{},
|
||||
informational_settings: %{},
|
||||
user_env: %{},
|
||||
process_info: %{}
|
||||
]
|
||||
|
||||
defmodule PrivateState do
|
||||
@moduledoc "State for the GenServer."
|
||||
|
||||
defstruct [
|
||||
subscribers: [],
|
||||
bot_state: %Farmbot.BotState{}
|
||||
]
|
||||
|
||||
@typedoc "Private State."
|
||||
@type t :: %__MODULE__{subscribers: [GenServer.server],
|
||||
bot_state: Farmbot.BotState.t}
|
||||
end
|
||||
|
||||
@doc "Start the Bot State Server."
|
||||
def start_link(opts \\ []) do
|
||||
GenServer.start_link(__MODULE__, [], opts)
|
||||
def start_link(opts) do
|
||||
GenStage.start_link(__MODULE__, [], opts)
|
||||
end
|
||||
|
||||
def init([]) do
|
||||
{:ok, %PrivateState{}}
|
||||
{:producer_consumer, struct(__MODULE__), subscribe_to: [Farmbot.Firmware]}
|
||||
end
|
||||
|
||||
# This is the signature of an update to a key in the bot's state.
|
||||
# Only except updates from module backed by a process somewhere.
|
||||
def handle_cast({:update, module, value}, priv) when module in @update_mods do
|
||||
["Farmbot", "BotState", camel] = Module.split(module)
|
||||
new_bot_state = %{priv.bot_state | :"#{Macro.underscore(camel)}" => value}
|
||||
dispatch priv, new_bot_state
|
||||
def handle_events(events, _from, state) do
|
||||
state = do_handle(events, state)
|
||||
{:noreply, [state], state}
|
||||
end
|
||||
|
||||
def handle_call(:subscribe, {pid, _ref}, priv) do
|
||||
# Checks if this pid is already subscribed.
|
||||
if pid in priv.subscribers do
|
||||
{:reply, {:error, :already_subscribed}, priv}
|
||||
else
|
||||
send pid, {:bot_state, priv.bot_state}
|
||||
{:reply, :ok, %{priv | subscribers: [pid | priv.subscribers]}}
|
||||
end
|
||||
defp do_handle([], state), do: state
|
||||
|
||||
defp do_handle([{key, diff} | rest], state) do
|
||||
state = %{state | key => Map.merge(Map.get(state, key), diff)}
|
||||
do_handle(rest, state)
|
||||
end
|
||||
|
||||
def handle_call(:unsubscribe, {pid, _ref}, %{subscribers: subs} = priv) do
|
||||
actually_removed = pid in subs
|
||||
{:reply, actually_removed, %{priv | subscribers: List.delete(subs, pid)}}
|
||||
end
|
||||
|
||||
def handle_call(:force_dispatch, _from, priv) do
|
||||
dispatch priv, priv.bot_state
|
||||
{:reply, :ok, priv}
|
||||
end
|
||||
|
||||
# Dispatches a new state to all the subscribers.
|
||||
defp dispatch(%PrivateState{subscribers: subs} = priv, %__MODULE__{} = new) do
|
||||
for sub <- subs do
|
||||
send sub, {:bot_state, new}
|
||||
end
|
||||
{:noreply, %{priv | bot_state: new}}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
defmodule Farmbot.BotState.Configuration do
|
||||
@moduledoc "Externally Editable configuration data"
|
||||
alias Farmbot.System.ConfigStorage, as: CS
|
||||
|
||||
defstruct [
|
||||
os_auto_update: false,
|
||||
first_party_farmware: true,
|
||||
timezone: nil,
|
||||
firmware_hardware: nil
|
||||
]
|
||||
|
||||
@typedoc "Config data"
|
||||
@type t :: %__MODULE__{
|
||||
os_auto_update: boolean,
|
||||
first_party_farmware: boolean,
|
||||
}
|
||||
|
||||
use Farmbot.BotState.Lib.Partition
|
||||
|
||||
def save_state(%__MODULE__{} = pub) do
|
||||
CS.update_config_value(:bool, "settings", "os_auto_update", pub.os_auto_update)
|
||||
CS.update_config_value(:bool, "settings", "first_party_farmware", pub.first_party_farmware)
|
||||
CS.update_config_value(:string, "settings", "timezone", pub.timezone)
|
||||
end
|
||||
end
|
|
@ -1,65 +0,0 @@
|
|||
defmodule Farmbot.BotState.InformationalSettings do
|
||||
@moduledoc "Configuration data that should only be changed internally."
|
||||
|
||||
defmodule SyncStatus do
|
||||
@moduledoc "Enum of all available statuses for the sync message."
|
||||
|
||||
@statuses [
|
||||
:locked,
|
||||
:maintenance,
|
||||
:sync_error,
|
||||
:sync_now,
|
||||
:synced,
|
||||
:syncing,
|
||||
:unknown,
|
||||
]
|
||||
|
||||
@typedoc "Status of the sync bar"
|
||||
@type t :: :locked |
|
||||
:maintenance |
|
||||
:sync_error |
|
||||
:sync_now |
|
||||
:synced |
|
||||
:syncing |
|
||||
:unknown
|
||||
|
||||
def status(sts) when sts in @statuses do
|
||||
sts
|
||||
end
|
||||
|
||||
def status(unknown) when is_atom(unknown) or is_binary(unknown) do
|
||||
raise "unknown sync status: #{unknown}"
|
||||
end
|
||||
end
|
||||
|
||||
@version Mix.Project.config[:version]
|
||||
|
||||
defstruct [
|
||||
controller_version: @version,
|
||||
firmware_version: :disconnected,
|
||||
throttled: false,
|
||||
private_ip: :disconnected,
|
||||
sync_status: SyncStatus.status(:sync_now),
|
||||
busy: true
|
||||
]
|
||||
|
||||
@typedoc "Information Settings."
|
||||
@type t :: %__MODULE__{
|
||||
controller_version: Version.version,
|
||||
firmware_version: :disconnected | Version.version,
|
||||
throttled: boolean,
|
||||
private_ip: :disconnected | Version.version,
|
||||
sync_status: SyncStatus.t,
|
||||
busy: boolean
|
||||
}
|
||||
|
||||
use Farmbot.BotState.Lib.Partition
|
||||
|
||||
def set_busy(part, busy) do
|
||||
GenServer.call(part, {:set_busy, busy})
|
||||
end
|
||||
|
||||
def partition_call({:set_busy, busy}, _, state) do
|
||||
{:reply, :ok, %{state | busy: busy}}
|
||||
end
|
||||
end
|
|
@ -1,42 +0,0 @@
|
|||
defmodule Farmbot.BotState.Job do
|
||||
@moduledoc "Job that can be represented as a progress bar of some sort."
|
||||
|
||||
defmodule PercentProgress do
|
||||
@moduledoc "Progress represented 0-100"
|
||||
defstruct [
|
||||
status: :working,
|
||||
unit: :percent,
|
||||
percent: 0
|
||||
]
|
||||
|
||||
@typedoc "A Progress struct represented as a percentage"
|
||||
@type t :: %__MODULE__{
|
||||
status: Farmbot.BotState.Job.status,
|
||||
unit: :percent,
|
||||
percent: number
|
||||
}
|
||||
end
|
||||
|
||||
defmodule ByteProgress do
|
||||
@moduledoc "Progress represented as a number of bytes."
|
||||
|
||||
defstruct [
|
||||
status: :working,
|
||||
unit: :bytes,
|
||||
bytes: 0
|
||||
]
|
||||
|
||||
@typedoc "A Progress struct represented by number of bytes."
|
||||
@type t :: %__MODULE__{
|
||||
status: Farmbot.BotState.Job.status,
|
||||
unit: :bytes,
|
||||
bytes: number
|
||||
}
|
||||
end
|
||||
|
||||
@typedoc "Job struct"
|
||||
@type t :: PercentProgress.t | ByteProgress.t
|
||||
|
||||
@typedoc "Status of the job"
|
||||
@type status :: :working | :complete | :error
|
||||
end
|
|
@ -1,160 +0,0 @@
|
|||
defmodule Farmbot.BotState.Lib.Partition do
|
||||
@moduledoc "Common functionality for parts of the bot state."
|
||||
alias Farmbot.BotState
|
||||
|
||||
defmodule PrivateState do
|
||||
@moduledoc "Internal state to a Partition."
|
||||
|
||||
@enforce_keys [:bot_state_tracker, :public]
|
||||
defstruct [:bot_state_tracker, :public]
|
||||
|
||||
@typedoc "Public state."
|
||||
@type public :: map
|
||||
|
||||
@typedoc "Private State."
|
||||
@type t :: %__MODULE__{
|
||||
bot_state_tracker: Farmbot.BotState.state_server,
|
||||
public: public
|
||||
}
|
||||
end
|
||||
|
||||
@typedoc "Reply to a GenServer.call."
|
||||
@type reply :: {:reply, term, PrivateState.public}
|
||||
|
||||
@typedoc "Noreply for GenServer.cast or GenServer.info."
|
||||
@type noreply :: {:noreply, PrivateState.public}
|
||||
|
||||
@typedoc "Stop the Partition."
|
||||
@type stop :: {:stop, term, PrivateState.public} | {:stop, term}
|
||||
|
||||
@doc "Start this Partition GenServer."
|
||||
@callback start_link(BotState.server, GenServer.options) :: GenServer.on_start
|
||||
|
||||
@doc "optional callback called on init."
|
||||
@callback partition_init(PrivateState.t) :: {:ok, PrivateState.t} | {:stop, term}
|
||||
|
||||
@doc "Optional callback for handle_call."
|
||||
@callback partition_call(term, GenServer.from, PrivateState.public) :: reply | noreply | stop
|
||||
|
||||
@doc "Optional callback for handle_cast."
|
||||
@callback partition_cast(term, PrivateState.public) :: noreply | stop
|
||||
|
||||
@doc "Optional callback for handle_info."
|
||||
@callback partition_info(term, PrivateState.public) :: noreply | stop
|
||||
|
||||
@doc "Otional callback for saveing state."
|
||||
@callback save_state(PrivateState.public) :: :ok | :error
|
||||
|
||||
@optional_callbacks [
|
||||
partition_init: 1,
|
||||
partition_call: 3,
|
||||
partition_info: 2,
|
||||
partition_cast: 2,
|
||||
save_state: 1
|
||||
]
|
||||
|
||||
@doc "Dispatches to the bot state tracker."
|
||||
@spec dispatch(PrivateState.t) :: {:noreply, PrivateState.t}
|
||||
def dispatch(private_state)
|
||||
def dispatch(%PrivateState{} = priv) do
|
||||
GenServer.cast(priv.bot_state_tracker, {:update, priv.public.__struct__, priv.public})
|
||||
{:noreply, priv}
|
||||
end
|
||||
|
||||
@doc "Dispatches to the bot state tracker, and replies with `reply`"
|
||||
@spec dispatch(term, PrivateState.t) :: {:reply, term, PrivateState.t}
|
||||
def dispatch(reply, private_state)
|
||||
def dispatch(reply, %PrivateState{} = priv) do
|
||||
GenServer.cast(priv.bot_state_tracker, {:update, priv.public.__struct__, priv.public})
|
||||
{:reply, reply, priv}
|
||||
end
|
||||
|
||||
@doc false
|
||||
defmacro __using__(_opts) do
|
||||
quote do
|
||||
|
||||
alias Farmbot.BotState.Lib.Partition
|
||||
import Partition
|
||||
@behaviour Partition
|
||||
|
||||
alias Partition.PrivateState
|
||||
|
||||
use GenServer
|
||||
require Logger
|
||||
|
||||
@doc "Start the partition."
|
||||
def start_link(bot_state_tracker, opts) do
|
||||
GenServer.start_link(__MODULE__, bot_state_tracker, opts)
|
||||
end
|
||||
|
||||
def init(bot_state_tracker) do
|
||||
initial = %PrivateState{public: struct(__MODULE__),
|
||||
bot_state_tracker: bot_state_tracker}
|
||||
partition_init(initial)
|
||||
end
|
||||
|
||||
def handle_call(call, from, %PrivateState{} = priv) do
|
||||
case partition_call(call, from, priv.public) do
|
||||
{:reply, reply, pub} -> dispatch reply, %{priv | public: pub}
|
||||
{:noreply, pub} ->
|
||||
save_public_data(pub)
|
||||
dispatch %{priv | public: pub}
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
def handle_cast(cast, %PrivateState{} = priv) do
|
||||
case partition_cast(cast, priv.public) do
|
||||
{:noreply, pub} ->
|
||||
save_public_data(pub)
|
||||
dispatch %{priv | public: pub}
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info(info, %PrivateState{} = priv) do
|
||||
case partition_info(info, priv.public) do
|
||||
{:noreply, pub} ->
|
||||
save_public_data(pub)
|
||||
dispatch %{priv | public: pub}
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def partition_init(%PrivateState{} = priv), do: {:ok, priv}
|
||||
|
||||
@doc false
|
||||
def partition_call(call, _from, public) do
|
||||
Logger.error "Unhandled call: #{inspect call}"
|
||||
{:stop, {:unhandled_call, call}, public}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def partition_cast(cast, public) do
|
||||
Logger.warn "Unhandled cast: #{inspect cast}"
|
||||
{:noreply, public}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def partition_info(info, public) do
|
||||
Logger.warn "Unhandled info: #{inspect info}"
|
||||
{:noreply, public}
|
||||
end
|
||||
|
||||
defp save_public_data(pub) do
|
||||
if function_exported?(__MODULE__, :save_state, 1) do
|
||||
:ok = apply(__MODULE__, :save_state, [pub])
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defoverridable [partition_init: 1,
|
||||
partition_call: 3,
|
||||
partition_cast: 2,
|
||||
partition_info: 2,
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,73 +0,0 @@
|
|||
defmodule Farmbot.BotState.LocationData do
|
||||
@moduledoc "Data about the bot's location in space"
|
||||
|
||||
defmodule Vec3 do
|
||||
@moduledoc "3 Position Vector used for locations"
|
||||
defstruct [:x, :y, :z]
|
||||
|
||||
@typedoc "x position."
|
||||
@type x :: number
|
||||
|
||||
@typedoc "y position."
|
||||
@type y :: number
|
||||
|
||||
@typedoc "z position."
|
||||
@type z :: number
|
||||
|
||||
@typedoc "3 Position vector used for location data"
|
||||
@type t :: %__MODULE__{x: x , y: y , z: z }
|
||||
|
||||
@doc "Builds a new 3 position vector."
|
||||
@spec new(x, y, z) :: t
|
||||
def new(x, y, z), do: %__MODULE__{x: x, y: y, z: z}
|
||||
end
|
||||
|
||||
defstruct [
|
||||
position: Vec3.new(-1, -1, -1),
|
||||
scaled_encoders: Vec3.new(-1, -1, -1),
|
||||
raw_encoders: Vec3.new(-1, -1, -1),
|
||||
end_stops: "-1-1-1-1-1-1"
|
||||
]
|
||||
|
||||
@typedoc "Data about the bot's position."
|
||||
@type t :: %__MODULE__{
|
||||
position: Vec3.t,
|
||||
scaled_encoders: Vec3.t,
|
||||
raw_encoders: Vec3.t,
|
||||
end_stops: binary
|
||||
}
|
||||
|
||||
use Farmbot.BotState.Lib.Partition
|
||||
|
||||
def report_current_position(part, x, y, z) do
|
||||
GenServer.call(part, {:report_current_position, Vec3.new(x,y,z)})
|
||||
end
|
||||
|
||||
def report_encoder_position_scaled(part, x, y, z) do
|
||||
GenServer.call(part, {:report_encoder_position_scaled, Vec3.new(x,y,z)})
|
||||
end
|
||||
|
||||
def report_encoder_position_raw(part, x, y, z) do
|
||||
GenServer.call(part, {:report_encoder_position_raw, Vec3.new(x,y,z)})
|
||||
end
|
||||
|
||||
def report_end_stops(part, xa, xb, ya, yb, za, zb) do
|
||||
GenServer.call(part, {:report_end_stops, "#{xa}#{xb}#{ya}#{yb}#{za}#{zb}"})
|
||||
end
|
||||
|
||||
def partition_call({:report_current_position, pos}, _, state) do
|
||||
{:reply, :ok, %{state | position: pos}}
|
||||
end
|
||||
|
||||
def partition_call({:report_encoder_position_scaled, pos}, _, state) do
|
||||
{:reply, :ok, %{state | scaled_encoders: pos}}
|
||||
end
|
||||
|
||||
def partition_call({:report_encoder_position_raw, pos}, _, state) do
|
||||
{:reply, :ok, %{state | raw_encoders: pos}}
|
||||
end
|
||||
|
||||
def partition_call({:report_end_stops, stops}, _, state) do
|
||||
{:reply, :ok, %{state | end_stops: stops}}
|
||||
end
|
||||
end
|
|
@ -1,141 +0,0 @@
|
|||
defmodule Farmbot.BotState.McuParams do
|
||||
@moduledoc "Params for the firmware"
|
||||
|
||||
defstruct [
|
||||
:encoder_enabled_x,
|
||||
:encoder_enabled_y,
|
||||
:encoder_enabled_z,
|
||||
:encoder_invert_x,
|
||||
:encoder_invert_y,
|
||||
:encoder_invert_z,
|
||||
:encoder_missed_steps_decay_x,
|
||||
:encoder_missed_steps_decay_y,
|
||||
:encoder_missed_steps_decay_z,
|
||||
:encoder_missed_steps_max_x,
|
||||
:encoder_missed_steps_max_y,
|
||||
:encoder_missed_steps_max_z,
|
||||
:encoder_scaling_x,
|
||||
:encoder_scaling_y,
|
||||
:encoder_scaling_z,
|
||||
:encoder_type_x,
|
||||
:encoder_type_y,
|
||||
:encoder_type_z,
|
||||
:encoder_use_for_pos_x,
|
||||
:encoder_use_for_pos_y,
|
||||
:encoder_use_for_pos_z,
|
||||
:movement_axis_nr_steps_x,
|
||||
:movement_axis_nr_steps_y,
|
||||
:movement_axis_nr_steps_z,
|
||||
:movement_enable_endpoints_x,
|
||||
:movement_enable_endpoints_y,
|
||||
:movement_enable_endpoints_z,
|
||||
:movement_home_at_boot_x,
|
||||
:movement_home_at_boot_y,
|
||||
:movement_home_at_boot_z,
|
||||
:movement_home_up_x,
|
||||
:movement_home_up_y,
|
||||
:movement_home_up_z,
|
||||
:movement_invert_endpoints_x,
|
||||
:movement_invert_endpoints_y,
|
||||
:movement_invert_endpoints_z,
|
||||
:movement_invert_motor_x,
|
||||
:movement_invert_motor_y,
|
||||
:movement_invert_motor_z,
|
||||
:movement_keep_active_x,
|
||||
:movement_keep_active_y,
|
||||
:movement_keep_active_z,
|
||||
:movement_max_spd_x,
|
||||
:movement_max_spd_y,
|
||||
:movement_max_spd_z,
|
||||
:movement_min_spd_x,
|
||||
:movement_min_spd_y,
|
||||
:movement_min_spd_z,
|
||||
:movement_secondary_motor_invert_x,
|
||||
:movement_secondary_motor_x,
|
||||
:movement_steps_acc_dec_x,
|
||||
:movement_steps_acc_dec_y,
|
||||
:movement_steps_acc_dec_z,
|
||||
:movement_stop_at_home_x,
|
||||
:movement_stop_at_home_y,
|
||||
:movement_stop_at_home_z,
|
||||
:movement_stop_at_max_x,
|
||||
:movement_stop_at_max_y,
|
||||
:movement_stop_at_max_z,
|
||||
:movement_timeout_x,
|
||||
:movement_timeout_y,
|
||||
:movement_timeout_z,
|
||||
:param_mov_nr_retry,
|
||||
:param_e_stop_on_mov_err,
|
||||
:param_version,
|
||||
]
|
||||
|
||||
@typedoc "Params for the FW."
|
||||
@type t :: %__MODULE__{
|
||||
encoder_enabled_x: number,
|
||||
encoder_enabled_y: number,
|
||||
encoder_enabled_z: number,
|
||||
encoder_invert_x: number,
|
||||
encoder_invert_y: number,
|
||||
encoder_invert_z: number,
|
||||
encoder_missed_steps_decay_x: number,
|
||||
encoder_missed_steps_decay_y: number,
|
||||
encoder_missed_steps_decay_z: number,
|
||||
encoder_missed_steps_max_x: number,
|
||||
encoder_missed_steps_max_y: number,
|
||||
encoder_missed_steps_max_z: number,
|
||||
encoder_scaling_x: number,
|
||||
encoder_scaling_y: number,
|
||||
encoder_scaling_z: number,
|
||||
encoder_type_x: number,
|
||||
encoder_type_y: number,
|
||||
encoder_type_z: number,
|
||||
encoder_use_for_pos_x: number,
|
||||
encoder_use_for_pos_y: number,
|
||||
encoder_use_for_pos_z: number,
|
||||
movement_axis_nr_steps_x: number,
|
||||
movement_axis_nr_steps_y: number,
|
||||
movement_axis_nr_steps_z: number,
|
||||
movement_enable_endpoints_x: number,
|
||||
movement_enable_endpoints_y: number,
|
||||
movement_enable_endpoints_z: number,
|
||||
movement_home_at_boot_x: number,
|
||||
movement_home_at_boot_y: number,
|
||||
movement_home_at_boot_z: number,
|
||||
movement_home_up_x: number,
|
||||
movement_home_up_y: number,
|
||||
movement_home_up_z: number,
|
||||
movement_invert_endpoints_x: number,
|
||||
movement_invert_endpoints_y: number,
|
||||
movement_invert_endpoints_z: number,
|
||||
movement_invert_motor_x: number,
|
||||
movement_invert_motor_y: number,
|
||||
movement_invert_motor_z: number,
|
||||
movement_keep_active_x: number,
|
||||
movement_keep_active_y: number,
|
||||
movement_keep_active_z: number,
|
||||
movement_max_spd_x: number,
|
||||
movement_max_spd_y: number,
|
||||
movement_max_spd_z: number,
|
||||
movement_min_spd_x: number,
|
||||
movement_min_spd_y: number,
|
||||
movement_min_spd_z: number,
|
||||
movement_secondary_motor_invert_x: number,
|
||||
movement_secondary_motor_x: number,
|
||||
movement_steps_acc_dec_x: number,
|
||||
movement_steps_acc_dec_y: number,
|
||||
movement_steps_acc_dec_z: number,
|
||||
movement_stop_at_home_x: number,
|
||||
movement_stop_at_home_y: number,
|
||||
movement_stop_at_home_z: number,
|
||||
movement_stop_at_max_x: number,
|
||||
movement_stop_at_max_y: number,
|
||||
movement_stop_at_max_z: number,
|
||||
movement_timeout_x: number,
|
||||
movement_timeout_y: number,
|
||||
movement_timeout_z: number,
|
||||
param_mov_nr_retry: number,
|
||||
param_e_stop_on_mov_err: number,
|
||||
param_version: number,
|
||||
}
|
||||
use Farmbot.BotState.Lib.Partition
|
||||
end
|
|
@ -1,23 +0,0 @@
|
|||
defmodule Farmbot.BotState.Pin do
|
||||
@moduledoc "State of a pin."
|
||||
|
||||
@typedoc false
|
||||
@type digital :: 1
|
||||
@digital 1
|
||||
|
||||
@typedoc false
|
||||
@type pwm :: 0
|
||||
@pwm 0
|
||||
|
||||
@enforce_keys [:mode, :value]
|
||||
defstruct [:mode, :value]
|
||||
|
||||
@typedoc "Pin."
|
||||
@type t :: %__MODULE__{mode: digital | pwm, value: number}
|
||||
|
||||
@doc "Digital pin mode."
|
||||
def digital, do: @digital
|
||||
|
||||
@doc "Pwm pin mode."
|
||||
def pwm, do: @pwm
|
||||
end
|
|
@ -1,22 +0,0 @@
|
|||
defmodule Farmbot.BotState.ProcessInfo do
|
||||
@moduledoc "Info about long running processes."
|
||||
|
||||
defstruct [
|
||||
farmwares: %{}
|
||||
]
|
||||
|
||||
@typedoc "State of the Process Info server."
|
||||
@type t :: %__MODULE__{
|
||||
farmwares: %{optional(Farmware.name) => Farmware.t}
|
||||
}
|
||||
|
||||
use Farmbot.BotState.Lib.Partition
|
||||
|
||||
def update_farmwares(part, farmwares) do
|
||||
GenServer.cast(part, {:update_farmwares, farmwares})
|
||||
end
|
||||
|
||||
def partition_cast({:update_farmwares, farmwares}, state) do
|
||||
{:noreply, %{state | farmwares: farmwares}}
|
||||
end
|
||||
end
|
|
@ -1,37 +1,17 @@
|
|||
defmodule Farmbot.BotState.Supervisor do
|
||||
@moduledoc "Supervises BotState stuff."
|
||||
|
||||
use Supervisor
|
||||
alias Farmbot.BotState
|
||||
alias Farmbot.BotState.{
|
||||
InformationalSettings, Configuration, LocationData, ProcessInfo, McuParams,
|
||||
Transport
|
||||
}
|
||||
|
||||
alias Farmbot.Firmware, as: FW
|
||||
|
||||
@doc "Start the BotState stack."
|
||||
def start_link(token, opts \\ []) do
|
||||
def start_link(token, opts) do
|
||||
Supervisor.start_link(__MODULE__, token, opts)
|
||||
end
|
||||
|
||||
def init(token) do
|
||||
def init(_token) do
|
||||
children = [
|
||||
# BotState parts.
|
||||
worker(BotState, [[name: BotState]]),
|
||||
worker(InformationalSettings, [BotState, [name: InformationalSettings]]),
|
||||
worker(Configuration, [BotState, [name: Configuration]]),
|
||||
worker(LocationData, [BotState, [name: LocationData]]),
|
||||
worker(ProcessInfo, [BotState, [name: ProcessInfo]]),
|
||||
worker(McuParams, [BotState, [name: McuParams]]),
|
||||
|
||||
# Transport part.
|
||||
supervisor(Transport.Supervisor, [token, BotState, [name: Transport.Supervisor]]),
|
||||
|
||||
# Firmware part.
|
||||
supervisor(FW.Supervisor, [BotState, InformationalSettings, Configuration, LocationData, McuParams, [name: FW.Supervisor]]),
|
||||
supervisor(Farmbot.Firmware.Supervisor, [[name: Farmbot.Firmware.Supervisor]]),
|
||||
worker(Farmbot.BotState, [[name: Farmbot.BotState]]),
|
||||
worker(Farmbot.Logger, [[name: Farmbot.Logger]]),
|
||||
supervisor(Farmbot.BotState.Transport.Supervisor, [])
|
||||
]
|
||||
# We set one_for_all here, since all of these link to `BotState`
|
||||
supervise(children, [strategy: :one_for_all])
|
||||
supervise(children, strategy: :one_for_one)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
defmodule Farmbot.BotState.Transport do
|
||||
@moduledoc """
|
||||
Serializes Farmbot's state to be send out to any subscribed transports.
|
||||
"""
|
||||
|
||||
@doc "Start a transport."
|
||||
@callback start_link(Farmbot.Bootstrap.Authorization.token, Farmbot.BotState.state_server) :: GenServer.on_start()
|
||||
|
||||
@doc "Log a message."
|
||||
@callback log(Farmbot.Log.t) :: :ok
|
||||
|
||||
@doc "Emit a message."
|
||||
@callback emit(Farmbot.CeleryScript.Ast.t) :: :ok
|
||||
|
||||
@error_msg """
|
||||
Could not find :transport configuration.
|
||||
config.exs should have:
|
||||
|
||||
config: :farmbot, :transport, [
|
||||
# any transport modules here.
|
||||
]
|
||||
"""
|
||||
|
||||
@doc "All transports."
|
||||
def transports do
|
||||
Application.get_env(:farmbot, :transport) || raise @error_msg
|
||||
end
|
||||
|
||||
@doc "Emit a message over all transports."
|
||||
def emit(%Farmbot.CeleryScript.Ast{} = msg) do
|
||||
for transport <- transports() do
|
||||
:ok = transport.emit(msg)
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Log a message over all transports."
|
||||
def log(%Farmbot.Log{} = log) do
|
||||
for transport <- transports() do
|
||||
:ok = transport.log(log)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,107 +1,117 @@
|
|||
defmodule Farmbot.BotState.Transport.GenMqtt do
|
||||
@moduledoc "Default MQTT Transport."
|
||||
|
||||
@behaviour Farmbot.BotState.Transport
|
||||
use GenMQTT
|
||||
defmodule Farmbot.BotState.Transport.GenMQTT do
|
||||
@moduledoc "MQTT BotState Transport."
|
||||
use GenStage
|
||||
require Logger
|
||||
|
||||
# Callback function
|
||||
def emit(msg), do: emit(__MODULE__, msg)
|
||||
defmodule Client do
|
||||
@moduledoc "Underlying client for interfacing MQTT."
|
||||
use GenMQTT
|
||||
require Logger
|
||||
|
||||
@doc "Emit a message on `client`."
|
||||
def emit(client, msg) do
|
||||
GenMQTT.cast(client, {:emit, msg})
|
||||
@doc "Start a MQTT Client."
|
||||
def start_link(device, token, server) do
|
||||
GenMQTT.start_link(__MODULE__, {device, server}, [
|
||||
reconnect_timeout: 10_000,
|
||||
username: device,
|
||||
password: token,
|
||||
timeout: 10_000,
|
||||
host: server
|
||||
])
|
||||
end
|
||||
|
||||
@doc "Push a bot state message."
|
||||
def push_bot_state(client, state) do
|
||||
GenMQTT.cast(client, {:bot_state, state})
|
||||
end
|
||||
|
||||
@doc "Push a log message."
|
||||
def push_bot_log(client, log) do
|
||||
GenMQTT.cast(client, {:bot_log, log})
|
||||
end
|
||||
|
||||
def init({device, _server}) do
|
||||
{:ok, %{connected: false, device: device}}
|
||||
end
|
||||
|
||||
def on_connect_error(:invalid_credentials, state) do
|
||||
msg = """
|
||||
Failed to authenticate with the message broker!
|
||||
This is likely a problem with your server/broker configuration.
|
||||
"""
|
||||
Logger.error ">> #{msg}"
|
||||
Farmbot.System.factory_reset(msg)
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
def on_connect_error(reason, state) do
|
||||
Logger.error ">> Failed to connect to mqtt: #{inspect reason}"
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
def on_connect(state) do
|
||||
GenMQTT.subscribe(self(), [{bot_topic(state.device), 0}])
|
||||
Logger.info ">> Connected!"
|
||||
{:ok, %{state | connected: true}}
|
||||
end
|
||||
|
||||
def on_publish(["bot", _bot, "from_clients"], msg, state) do
|
||||
Logger.warn "not implemented yet: #{inspect msg}"
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
def handle_cast({:bot_state, bs}, state) do
|
||||
json = Poison.encode!(bs)
|
||||
GenMQTT.publish(self(), status_topic(state.device), json, 0, false)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_cast(_, %{connected: false} = state) do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_cast({:bot_log, log}, state) do
|
||||
json = Poison.encode!(log)
|
||||
GenMQTT.publish(self(), log_topic(state.device), json, 0, false)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
defp frontend_topic(bot), do: "bot/#{bot}/from_device"
|
||||
defp bot_topic(bot), do: "bot/#{bot}/from_clients"
|
||||
defp status_topic(bot), do: "bot/#{bot}/status"
|
||||
defp log_topic(bot), do: "bot/#{bot}/logs"
|
||||
end
|
||||
|
||||
# Callback function
|
||||
def log(log), do: log(__MODULE__, log)
|
||||
|
||||
@doc "Log a message on `client`."
|
||||
def log(client, log) do
|
||||
GenMQTT.cast(client, {:log, log})
|
||||
@doc "Start the MQTT Transport."
|
||||
def start_link(opts) do
|
||||
GenStage.start_link(__MODULE__, [], opts)
|
||||
end
|
||||
|
||||
# Callback function.
|
||||
def start_link(bin_token, bot_state_tracker, name \\ __MODULE__) do
|
||||
token = Farmbot.Jwt.decode!(bin_token)
|
||||
GenMQTT.start_link(__MODULE__, [token.bot, bot_state_tracker], build_opts(bin_token, token, name))
|
||||
def init([]) do
|
||||
token = Farmbot.System.ConfigStorage.get_config_value(:string, "authorization", "token")
|
||||
{:ok, %{bot: device, mqtt: mqtt_server}} = Farmbot.Jwt.decode(token)
|
||||
{:ok, client} = Client.start_link(device, token, mqtt_server)
|
||||
{:consumer, {%{client: client}, nil}, subscribe_to: [Farmbot.BotState, Farmbot.Logger]}
|
||||
end
|
||||
|
||||
## Server Implementation.
|
||||
|
||||
defmodule State do
|
||||
@moduledoc false
|
||||
defstruct [bot: nil, connected: false]
|
||||
def handle_events(events, {pid, _}, state) do
|
||||
case Process.info(pid)[:registered_name] do
|
||||
Farmbot.Logger -> handle_log_events(events, state)
|
||||
Farmbot.BotState -> handle_bot_state_events(events, state)
|
||||
end
|
||||
end
|
||||
|
||||
def init([bot, bot_state_tracker]) do
|
||||
:ok = Farmbot.BotState.subscribe(bot_state_tracker)
|
||||
{:ok, %State{bot: bot}}
|
||||
def handle_log_events(logs, {%{client: client} = internal_state, old_bot_state}) do
|
||||
for log <- logs do
|
||||
Client.push_bot_log(client, log)
|
||||
end
|
||||
{:noreply, [], {internal_state, old_bot_state}}
|
||||
end
|
||||
|
||||
def on_connect(state) do
|
||||
GenMQTT.subscribe(self(), [{bot_topic(state.bot), 0}])
|
||||
Logger.info ">> Connected!"
|
||||
{:ok, %{state | connected: true}}
|
||||
def handle_bot_state_events(events, {%{client: client} = internal_state, old_bot_state}) do
|
||||
new_bot_state = List.last(events)
|
||||
if new_bot_state != old_bot_state do
|
||||
Client.push_bot_state client, new_bot_state
|
||||
end
|
||||
{:noreply, [], {internal_state, new_bot_state}}
|
||||
end
|
||||
|
||||
def on_connect_error(:invalid_credentials, state) do
|
||||
msg = """
|
||||
Failed to authenticate with the message broker!
|
||||
This is likely a problem with your server/broker configuration.
|
||||
"""
|
||||
Logger.error ">> #{msg}"
|
||||
Farmbot.System.factory_reset(msg)
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
def on_connect_error(reason, state) do
|
||||
Logger.error ">> Failed to connect to mqtt: #{inspect reason}"
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
def on_publish(["bot", _bot, "from_clients"], msg, state) do
|
||||
Logger.warn "not implemented yet: #{inspect msg}"
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
def handle_info(_, %{connected: false} = state), do: {:ok, state}
|
||||
|
||||
def handle_info({:bot_state, bs}, state) do
|
||||
Logger.info "Got bot state update"
|
||||
json = Poison.encode!(bs)
|
||||
GenMQTT.publish(self(), status_topic(state.bot), json, 0, false)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_cast(_, %{connected: false} = state), do: {:noreply, state}
|
||||
|
||||
def handle_cast({:log, msg}, state) do
|
||||
json = Poison.encode! msg
|
||||
GenMQTT.publish(self(), log_topic(state.bot), json, 0, false)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_cast({:emit, msg}, state) do
|
||||
json = Poison.encode! msg
|
||||
GenMQTT.publish(self(), frontend_topic(state.bot), json, 0, false)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
defp frontend_topic(bot), do: "bot/#{bot}/from_device"
|
||||
defp bot_topic(bot), do: "bot/#{bot}/from_clients"
|
||||
defp status_topic(bot), do: "bot/#{bot}/status"
|
||||
defp log_topic(bot), do: "bot/#{bot}/logs"
|
||||
|
||||
defp build_opts(bin_token, %{mqtt: mqtt, bot: bot}, name) do
|
||||
[
|
||||
name: name,
|
||||
reconnect_timeout: 10_000,
|
||||
username: bot,
|
||||
password: bin_token,
|
||||
timeout: 10_000,
|
||||
host: mqtt
|
||||
]
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -1,22 +1,16 @@
|
|||
defmodule Farmbot.BotState.Transport.Supervisor do
|
||||
@moduledoc """
|
||||
Supervises services that communicate with the outside world.
|
||||
"""
|
||||
use Supervisor
|
||||
import Farmbot.BotState.Transport, only: [transports: 0]
|
||||
@moduledoc ""
|
||||
|
||||
@doc "Start the Transport Supervisor."
|
||||
def start_link(token, bot_state_tracker, opts \\ []) do
|
||||
Supervisor.start_link(__MODULE__, [token, bot_state_tracker], opts)
|
||||
use Supervisor
|
||||
|
||||
def start_link do
|
||||
Supervisor.start_link(__MODULE__, [], name: __MODULE__)
|
||||
end
|
||||
|
||||
def init([token, bot_state_tracker]) do
|
||||
# Start workers that consume the bots state, and push it somewhere else.
|
||||
children = Enum.map(transports(), fn(transport) ->
|
||||
worker(transport, [token, bot_state_tracker])
|
||||
end)
|
||||
|
||||
opts = [strategy: :one_for_one]
|
||||
supervise(children, opts)
|
||||
def init([]) do
|
||||
:farmbot
|
||||
|> Application.get_env(:transport)
|
||||
|> Enum.map(&worker(&1, [[name: &1]]))
|
||||
|> supervise([strategy: :one_for_one])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,76 +1,41 @@
|
|||
defmodule Farmbot.Firmware do
|
||||
@moduledoc "Allows communication with the firmware."
|
||||
|
||||
use GenServer
|
||||
use GenStage
|
||||
require Logger
|
||||
|
||||
alias Farmbot.BotState
|
||||
alias BotState.{
|
||||
InformationalSettings,
|
||||
LocationData
|
||||
}
|
||||
@handler Application.get_env(:farmbot, :behaviour)[:firmware_handler] || raise "No fw handler."
|
||||
|
||||
@doc "Public API for handling a gcode."
|
||||
def handle_gcode(firmware, code), do: GenServer.call(firmware, {:handle_gcode, code})
|
||||
|
||||
def start_link(bot_state, informational_settings, configuration, location_data, mcu_params, handler_mod, opts) do
|
||||
GenServer.start_link(__MODULE__, [bot_state, informational_settings, configuration, location_data, mcu_params, handler_mod], opts)
|
||||
@doc "Start the firmware services."
|
||||
def start_link(opts) do
|
||||
GenStage.start_link(__MODULE__, [], opts)
|
||||
end
|
||||
|
||||
defmodule State do
|
||||
defstruct [
|
||||
:bot_state,
|
||||
:informational_settings,
|
||||
:configuration,
|
||||
:location_data,
|
||||
:mcu_params,
|
||||
:handler_mod,
|
||||
:handler
|
||||
]
|
||||
def init([]) do
|
||||
{:producer_consumer, [], subscribe_to: [@handler]}
|
||||
end
|
||||
|
||||
def init([bot_state, informational_settings, configuration, location_data, mcu_params, handler_mod]) do
|
||||
{:ok, handler} = handler_mod.start_link(self(), name: handler_mod)
|
||||
Process.link(handler)
|
||||
s = %State{
|
||||
bot_state: bot_state,
|
||||
informational_settings: informational_settings,
|
||||
configuration: configuration,
|
||||
location_data: location_data,
|
||||
mcu_params: mcu_params,
|
||||
handler_mod: handler_mod,
|
||||
handler: handler
|
||||
}
|
||||
{:ok, s}
|
||||
def handle_events(gcodes, _from, state) do
|
||||
{:noreply, handle_gcodes(gcodes), state}
|
||||
end
|
||||
|
||||
def handle_call({:handle_gcode, :idle}, _, state) do
|
||||
reply = InformationalSettings.set_busy(state.informational_settings, false)
|
||||
{:reply, reply, state}
|
||||
def handle_gcodes(codes, acc \\ [])
|
||||
def handle_gcodes([], acc), do: Enum.reverse(acc)
|
||||
def handle_gcodes([code | rest], acc) do
|
||||
res = handle_gcode(code)
|
||||
if res do
|
||||
handle_gcodes(rest, [res | acc])
|
||||
else
|
||||
handle_gcodes(rest, acc)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_call({:handle_gcode, {:report_current_position, x, y, z}}, _, state) do
|
||||
reply = LocationData.report_current_position(state.location_data, x, y, z)
|
||||
{:reply, reply, state}
|
||||
def handle_gcode({:report_current_position, x, y, z}) do
|
||||
{:location_data, %{position: %{x: x, y: y, z: z}}}
|
||||
end
|
||||
|
||||
def handle_call({:handle_gcode, {:report_encoder_position_scaled, x, y, z}}, _, state) do
|
||||
reply = LocationData.report_encoder_position_scaled(state.location_data, x, y, z)
|
||||
{:reply, reply, state}
|
||||
end
|
||||
|
||||
def handle_call({:handle_gcode, {:report_encoder_position_raw, x, y, z}}, _, state) do
|
||||
reply = LocationData.report_encoder_position_raw(state.location_data, x, y, z)
|
||||
{:reply, reply, state}
|
||||
end
|
||||
|
||||
def handle_call({:handle_gcode, {:report_end_stops, xa, xb, ya, yb, za, zb}}, _, state) do
|
||||
reply = LocationData.report_end_stops(state.location_data, xa, xb, ya, yb, za, zb)
|
||||
{:reply, reply, state}
|
||||
end
|
||||
|
||||
def handle_call({:handle_gcode, code}, _, state) do
|
||||
Logger.warn "Got misc gcode: #{inspect code}"
|
||||
{:reply, {:error, :unhandled}, state}
|
||||
def handle_gcode(_code) do
|
||||
# Logger.warn "unhandled code: #{inspect code}"
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,11 +1,27 @@
|
|||
defmodule Farmbot.Firmware.StubHandler do
|
||||
@moduledoc "Stubs out firmware functionality when you don't have an arduino."
|
||||
use GenServer
|
||||
use GenStage
|
||||
require Logger
|
||||
|
||||
@doc "Start the firmware handler stub."
|
||||
def start_link(firmware, opts) do
|
||||
def start_link(opts) do
|
||||
Logger.warn("Firmware is being stubbed.")
|
||||
GenServer.start_link(__MODULE__, firmware, opts)
|
||||
GenStage.start_link(__MODULE__, [], opts)
|
||||
end
|
||||
|
||||
def write(handler, string) do
|
||||
GenStage.call(handler, {:write, string})
|
||||
end
|
||||
|
||||
def init([]) do
|
||||
{:producer, []}
|
||||
end
|
||||
|
||||
def handle_demand(_amnt, state) do
|
||||
{:noreply, [], state}
|
||||
end
|
||||
|
||||
def handle_call({:write, _string}, _from, state) do
|
||||
{:reply, :ok, state}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,14 +5,15 @@ defmodule Farmbot.Firmware.Supervisor do
|
|||
@error_msg "Please configure a Firmware handler."
|
||||
|
||||
@doc "Start the Firmware Supervisor."
|
||||
def start_link(bot_state, informational_settings, configuration, location_data, mcu_params, opts \\ []) do
|
||||
Supervisor.start_link(__MODULE__, [bot_state, informational_settings, configuration, location_data, mcu_params], opts)
|
||||
def start_link(opts \\ []) do
|
||||
Supervisor.start_link(__MODULE__, [], opts)
|
||||
end
|
||||
|
||||
def init([bot_state, informational_settings, configuration, location_data, mcu_params]) do
|
||||
def init([]) do
|
||||
handler_mod = Application.get_env(:farmbot, :behaviour)[:firmware_handler] || raise @error_msg
|
||||
children = [
|
||||
worker(Farmbot.Firmware, [bot_state, informational_settings, configuration, location_data, mcu_params, handler_mod, [name: Farmbot.Firmware]])
|
||||
worker(handler_mod, [[name: handler_mod]]),
|
||||
worker(Farmbot.Firmware, [[name: Farmbot.Firmware]])
|
||||
]
|
||||
opts = [strategy: :one_for_one]
|
||||
supervise(children, opts)
|
||||
|
|
|
@ -3,21 +3,20 @@ defmodule Farmbot.Firmware.UartHandler do
|
|||
Handles communication between farmbot and uart devices
|
||||
"""
|
||||
|
||||
use GenServer
|
||||
use GenStage
|
||||
alias Nerves.UART
|
||||
alias Farmbot.Firmware.Gcode.Parser
|
||||
require Logger
|
||||
|
||||
@doc """
|
||||
Writes a string to the uart line
|
||||
"""
|
||||
def write(handler, string) do
|
||||
GenServer.call(handler, {:write, string}, :infinity)
|
||||
GenStage.call(handler, {:write, string}, :infinity)
|
||||
end
|
||||
|
||||
@doc "Starts a UART GenServer"
|
||||
def start_link(firmware, opts) do
|
||||
GenServer.start_link(__MODULE__, firmware, opts)
|
||||
@doc "Starts a UART Firmware Handler."
|
||||
def start_link(opts) do
|
||||
GenStage.start_link(__MODULE__, [], opts)
|
||||
end
|
||||
|
||||
## Private
|
||||
|
@ -25,17 +24,17 @@ defmodule Farmbot.Firmware.UartHandler do
|
|||
defmodule State do
|
||||
@moduledoc false
|
||||
defstruct [
|
||||
:firmware,
|
||||
:nerves
|
||||
:nerves,
|
||||
:codes
|
||||
]
|
||||
end
|
||||
|
||||
def init(firmware) do
|
||||
def init([]) do
|
||||
tty = Application.get_env(:farmbot, :uart_handler)[:tty] || raise "Please configure uart handler!"
|
||||
{:ok, nerves} = UART.start_link()
|
||||
Process.link(nerves)
|
||||
case open_tty(nerves, tty) do
|
||||
:ok -> {:ok, %State{firmware: firmware, nerves: nerves}}
|
||||
:ok -> {:producer, %State{nerves: nerves, codes: []}}
|
||||
err -> {:stop, err, :no_state}
|
||||
end
|
||||
end
|
||||
|
@ -62,19 +61,25 @@ defmodule Farmbot.Firmware.UartHandler do
|
|||
{:stop, {:error, reason}, state}
|
||||
end
|
||||
|
||||
def handle_info({:nerves_uart, _, bin}, state) do
|
||||
case Parser.parse_code(bin) do
|
||||
{:unhandled_gcode, code_str} ->
|
||||
Logger.warn "Got unhandled code: #{code_str}"
|
||||
{_q, gcode} ->
|
||||
_reply = Farmbot.Firmware.handle_gcode(state.firmware, gcode)
|
||||
end
|
||||
{:noreply, state}
|
||||
def handle_info({:nerves_uart, _, {:unhandled_gcode, _code_str}}, state) do
|
||||
{:noreply, [], state}
|
||||
end
|
||||
|
||||
def handle_info({:nerves_uart, _, {_q, gcode}}, state) do
|
||||
do_dispatch([gcode | state.codes], state)
|
||||
end
|
||||
|
||||
def handle_call({:write, stuff}, _from, state) do
|
||||
UART.write(state.nerves, stuff)
|
||||
{:reply, :ok, state}
|
||||
{:reply, :ok, [], state}
|
||||
end
|
||||
|
||||
def handle_demand(_amnt, state) do
|
||||
do_dispatch(state.codes, state)
|
||||
end
|
||||
|
||||
defp do_dispatch(codes, state) do
|
||||
{:noreply, Enum.reverse(codes), %{state | codes: []}}
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
defmodule Farmbot.Firmware.UartHandler.Framinig do
|
||||
@behaviour Nerves.UART.Framing
|
||||
import Farmbot.Firmware.Gcode.Parser
|
||||
|
||||
@moduledoc """
|
||||
Each message is one line. This framer appends and removes newline sequences
|
||||
as part of the framing. Buffering is performed internally, so users can get
|
||||
the complete messages under normal circumstances. Attention should be paid
|
||||
to the following:
|
||||
|
||||
1. Lines must have a fixed max length so that a misbehaving sender can't
|
||||
cause unbounded buffer expansion. When the max length is passed, a
|
||||
`{:partial, data}` is reported. The application can decide what to do with
|
||||
this.
|
||||
2. The separation character varies depending on the target device. Some
|
||||
devices require "\\r\\n" sequences, so be sure to specify this. Currently
|
||||
only one or two character separators are supported.
|
||||
3. It may be desirable to set a `:rx_framer_timeout` to prevent
|
||||
characters received in error from collecting during idle times. When the
|
||||
receive timer expires, `{:partial, data}` is reported.
|
||||
4. Line separators must be ASCII characters (0-127) or be valid UTF-8
|
||||
sequences. If the device only sends ASCII, high characters (128-255)
|
||||
should work as well. [Note: please report if using extended
|
||||
characters.]
|
||||
"""
|
||||
|
||||
defmodule State do
|
||||
@moduledoc false
|
||||
defstruct [
|
||||
max_length: nil,
|
||||
separator: nil,
|
||||
processed: <<>>,
|
||||
in_process: <<>>
|
||||
]
|
||||
end
|
||||
|
||||
def init(args) do
|
||||
max_length = Keyword.get(args, :max_length, 4096)
|
||||
separator = Keyword.get(args, :separator, "\n")
|
||||
|
||||
state = %State{max_length: max_length, separator: separator}
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
def add_framing(data, state) do
|
||||
{:ok, data <> state.separator, state}
|
||||
end
|
||||
|
||||
def remove_framing(data, state) do
|
||||
{new_processed, new_in_process, lines} =
|
||||
process_data(state.separator,
|
||||
byte_size(state.separator),
|
||||
state.max_length,
|
||||
state.processed,
|
||||
state.in_process <> data, [])
|
||||
new_state = %{state | processed: new_processed, in_process: new_in_process}
|
||||
rc = if buffer_empty?(new_state), do: :ok, else: :in_frame
|
||||
{rc, lines, new_state}
|
||||
end
|
||||
|
||||
def frame_timeout(state) do
|
||||
partial_line = {:partial, state.processed <> state.in_process}
|
||||
new_state = %{state | processed: <<>>, in_process: <<>>}
|
||||
{:ok, [partial_line], new_state}
|
||||
end
|
||||
|
||||
def flush(direction, state) when direction == :receive or direction == :both do
|
||||
%{state | processed: <<>>, in_process: <<>>}
|
||||
end
|
||||
def flush(_direction, state) do
|
||||
state
|
||||
end
|
||||
|
||||
def buffer_empty?(state) do
|
||||
state.processed == <<>> and state.in_process == <<>>
|
||||
end
|
||||
|
||||
# Handle not enough data case
|
||||
defp process_data(_separator, sep_length, _max_length, processed, to_process, lines)
|
||||
when byte_size(to_process) < sep_length do
|
||||
{processed, to_process, lines}
|
||||
end
|
||||
|
||||
# Process data until separator or next char
|
||||
defp process_data(separator, sep_length, max_length, processed, to_process, lines) do
|
||||
case to_process do
|
||||
# Handle separater
|
||||
<<^separator::binary-size(sep_length), rest::binary>> ->
|
||||
new_lines = lines ++ [parse_code(processed)]
|
||||
process_data(separator, sep_length, max_length, <<>>, rest, new_lines)
|
||||
# Handle line too long case
|
||||
to_process when byte_size(processed) == max_length and to_process != <<>> ->
|
||||
new_lines = lines ++ [{:partial, processed}]
|
||||
process_data(separator, sep_length, max_length, <<>>, to_process, new_lines)
|
||||
# Handle next char
|
||||
<<next_char::binary-size(1), rest::binary>> ->
|
||||
process_data(separator, sep_length, max_length, processed <> next_char, rest, lines)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -284,7 +284,6 @@ defmodule Farmbot.HTTP do
|
|||
opts = opts |> Keyword.put(:timeout, :infinity)
|
||||
#TODO Fix this.
|
||||
url = "https:" <> Farmbot.Jwt.decode!(tkn).iss <> url
|
||||
IO.inspect(url)
|
||||
do_normal_request({method, url, body, headers, opts, from}, nil, state)
|
||||
end
|
||||
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
defmodule Farmbot.Log.Meta do
|
||||
defstruct [:x, :y, :z]
|
||||
end
|
||||
|
||||
defmodule Farmbot.Log do
|
||||
@moduledoc "Farmbot Log Object."
|
||||
alias Farmbot.Log.Meta
|
||||
|
||||
defstruct [
|
||||
:meta,
|
||||
:message,
|
||||
:created_at,
|
||||
:channels
|
||||
meta: %Meta{x: -1, y: -1, z: -1},
|
||||
message: nil,
|
||||
created_at: nil,
|
||||
channels: []
|
||||
]
|
||||
end
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
defmodule Farmbot.Logger do
|
||||
@moduledoc "Logger."
|
||||
use GenStage
|
||||
alias Farmbot.Log
|
||||
|
||||
@doc "Start Logging Services."
|
||||
def start_link(opts \\ []) do
|
||||
GenStage.start_link(__MODULE__, [], opts)
|
||||
end
|
||||
|
||||
def init([]) do
|
||||
Logger.add_backend(Logger.Backends.Farmbot, [])
|
||||
{:producer_consumer, %{meta: %Log.Meta{x: -1, y: -1, z: -1}}, subscribe_to: [Farmbot.Firmware]}
|
||||
end
|
||||
|
||||
def handle_demand(_, state) do
|
||||
{:noreply, [], state}
|
||||
end
|
||||
|
||||
def handle_events(gcodes, _from, state) do
|
||||
{x, y, z} = Enum.find_value(gcodes, fn(code) ->
|
||||
case code do
|
||||
{:report_current_position, x, y, z} -> {x, y, z}
|
||||
_ -> false
|
||||
end
|
||||
end)
|
||||
{:noreply, [], %{state | meta: %{state.meta | x: x, y: y, z: z}}}
|
||||
end
|
||||
|
||||
def handle_info({:log, log}, state) do
|
||||
{:noreply, [%{log | meta: state.meta}], state}
|
||||
end
|
||||
|
||||
def terminate(_, _state) do
|
||||
Logger.remove_backend(Logger.Backends.Farmbot)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,45 @@
|
|||
defmodule Logger.Backends.Farmbot do
|
||||
@moduledoc "Farmbot Loggerr Backend."
|
||||
alias Farmbot.Log
|
||||
|
||||
def init(_opts) do
|
||||
{:ok, %{}}
|
||||
end
|
||||
|
||||
def handle_event({_level, gl, {Logger, _, _, _}}, state) when node(gl) != node() do
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
def handle_event({level, _gl, {Logger, message, {{year, month, day}, {hour, minute, second, _millisecond}}, metadata}}, state) do
|
||||
mod_split = (metadata[:module] || Logger) |> Module.split()
|
||||
case mod_split do
|
||||
["Farmbot" | _] ->
|
||||
t = %DateTime{year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
calendar: Calendar.ISO,
|
||||
microsecond: {0, 0},
|
||||
second: second,
|
||||
std_offset: 0,
|
||||
time_zone: "Etc/UTC",
|
||||
utc_offset: 0,
|
||||
zone_abbr: "UTC"} |> DateTime.to_iso8601
|
||||
l = %Log{message: message, channels: [level], created_at: t}
|
||||
GenStage.async_info(Farmbot.Logger, {:log, l})
|
||||
_ -> :ok
|
||||
end
|
||||
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
def handle_event(:flush, state) do
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
def handle_info(_, state) do
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
end
|
2
mix.exs
2
mix.exs
|
@ -70,6 +70,8 @@ defmodule Farmbot.Mixfile do
|
|||
{:gen_mqtt, "~> 0.3.1"},
|
||||
{:vmq_commons, github: "farmbot-labs/vmq_commons", override: true},
|
||||
|
||||
{:gen_stage, "~> 0.12"},
|
||||
|
||||
{:poison, "~> 3.0"},
|
||||
{:ex_json_schema, "~> 0.5.3"},
|
||||
{:rsa, "~> 0.0.1"},
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
"exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [], [], "hexpm"},
|
||||
"gen_mqtt": {:hex, :gen_mqtt, "0.3.1", "6ce6af7c2bcb125d5b4125c67c5ab1f29bcec2638236509bcc6abf510a6661ed", [], [{:vmq_commons, "1.0.0", [hex: :vmq_commons, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"gen_stage": {:hex, :gen_stage, "0.12.2", "e0e347cbb1ceb5f4e68a526aec4d64b54ad721f0a8b30aa9d28e0ad749419cbb", [], [], "hexpm"},
|
||||
"gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [], [], "hexpm"},
|
||||
"hackney": {:hex, :hackney, "1.9.0", "51c506afc0a365868469dcfc79a9d0b94d896ec741cfd5bd338f49a5ec515bfe", [], [{:certifi, "2.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"httpoison": {:hex, :httpoison, "0.13.0", "bfaf44d9f133a6599886720f3937a7699466d23bb0cd7a88b6ba011f53c6f562", [], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
|
|
Loading…
Reference in New Issue