prepare for merge

pull/363/head
Connor Rigby 2017-10-04 19:11:20 -07:00
commit 345287a534
26 changed files with 409 additions and 939 deletions

View File

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

View File

@ -15,7 +15,7 @@ config :farmbot, :init, [
# Transports.
config :farmbot, :transport, [
Farmbot.BotState.Transport.GenMqtt
Farmbot.BotState.Transport.GenMQTT
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"},

View File

@ -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"},