Botstate refactor (#645)

* Refactor PinBindings

* Refactor logging to not require Farmbot.Registry

* Fix AMQP bot_state_transport and log_transport

* Update to use floats everywhere

* Refactor Farmbot.Firmware

* Write Firmware Tests

* dipping into FarmbotOS finally

* Cleanup peripheral_worker error message

* Implement remaining CeleryScript RPCs

* Refactor job progress

* Image Upload status notifications updates

* Fix compiler warnings and things

* Update Firmware submodule

* Fix FarmbotEXT tests
pull/974/head
Connor Rigby 2018-11-15 10:24:09 -08:00 committed by Connor Rigby
parent 3b8f8d591f
commit f2b8abd692
No known key found for this signature in database
GPG Key ID: 29A88B24B70456E0
164 changed files with 5636 additions and 3648 deletions

View File

@ -1,4 +1,4 @@
.PHONY: all clean
.PHONY: all clean format
.DEFAULT_GOAL: all
MIX_ENV := $(MIX_ENV)
@ -15,6 +15,7 @@ endif
PROJECTS := farmbot_celery_script \
farmbot_core \
farmbot_ext \
farmbot_firmware \
farmbot_os
all: help
@ -39,3 +40,9 @@ clean: clean_other_branch
rm -rf $$project/deps ; \
rm -rf $$project/priv/*.so ; \
done
format:
@for project in $(PROJECTS) ; do \
echo formatting $$project ; \
cd $$project && mix format && cd .. ; \
done

View File

@ -1,3 +1,3 @@
[
inputs: ["*.{ex,exs}", "{config,priv,lib,test}/**/*.{ex,exs}"],
inputs: ["*.{ex,exs}", "{config,priv,lib,test}/**/*.{ex,exs}"]
]

View File

@ -26,8 +26,7 @@ defmodule Farmbot.CeleryScript.RunTime do
:find_home,
:toggle_pin,
:zero,
:calibrate,
:calibrate
]
@kinds_aloud_while_locked [
@ -72,6 +71,7 @@ defmodule Farmbot.CeleryScript.RunTime do
defmodule Monitor do
defstruct [:pid, :ref, :index]
def new(pid, index) do
ref = Process.monitor(pid)
%Monitor{ref: ref, pid: pid, index: index}
@ -235,16 +235,20 @@ defmodule Farmbot.CeleryScript.RunTime do
cleanup = fn proc, state ->
ProcStorage.delete(state.proc_storage, id)
state = case Enum.find(state.monitors, fn({_, %{index: index}}) -> index == id end) do
{pid, mon} ->
Process.demonitor(mon.ref)
%{state | monitors: Map.delete(state.monitors, pid)}
_ -> state
end
state =
case Enum.find(state.monitors, fn {_, %{index: index}} -> index == id end) do
{pid, mon} ->
Process.demonitor(mon.ref)
%{state | monitors: Map.delete(state.monitors, pid)}
state = if proc.ref == state.fw_proc,
do: %{state | fw_proc: nil},
else: state
_ ->
state
end
state =
if proc.ref == state.fw_proc,
do: %{state | fw_proc: nil},
else: state
{:reply, proc, state}
end
@ -275,15 +279,17 @@ defmodule Farmbot.CeleryScript.RunTime do
end
mon = state.monitors[pid]
state = if mon do
case ProcStorage.lookup(state.proc_storage, mon.index) do
%FarmProc{status: :crashed} = proc -> cleanup.(proc, mon.index, state)
%FarmProc{status: :done} = proc -> cleanup.(proc, mon.index, state)
_ -> state
state =
if mon do
case ProcStorage.lookup(state.proc_storage, mon.index) do
%FarmProc{status: :crashed} = proc -> cleanup.(proc, mon.index, state)
%FarmProc{status: :done} = proc -> cleanup.(proc, mon.index, state)
_ -> state
end
else
state
end
else
state
end
{:noreply, %{state | monitors: Map.delete(state.monitors, pid)}}
end

View File

@ -128,8 +128,7 @@ defmodule Farmbot.CeleryScript.RunTime.FarmProc do
kind = get_kind(farm_proc, pc_ptr)
# TODO Connor 07-31-2018: why do i have to load the module here?
available? =
Code.ensure_loaded?(InstructionSet) and
function_exported?(InstructionSet, kind, 1)
Code.ensure_loaded?(InstructionSet) and function_exported?(InstructionSet, kind, 1)
unless available? do
exception(farm_proc, "No implementation for: #{kind}")
@ -216,8 +215,7 @@ defmodule Farmbot.CeleryScript.RunTime.FarmProc do
) do
cell = get_cell_by_address(farm_proc, location)
cell[field] ||
exception(farm_proc, "no field called: #{field} at #{inspect(location)}")
cell[field] || exception(farm_proc, "no field called: #{field} at #{inspect(location)}")
end
@spec get_cell_attr_as_pointer(FarmProc.t(), Pointer.t(), atom) :: Pointer.t()
@ -271,7 +269,6 @@ defmodule Farmbot.CeleryScript.RunTime.FarmProc do
%FarmProc{} = farm_proc,
%Pointer{page_address: page, heap_address: %Address{} = ha}
) do
get_heap_by_page_index(farm_proc, page)[ha] ||
exception(farm_proc, "bad address")
get_heap_by_page_index(farm_proc, page)[ha] || exception(farm_proc, "bad address")
end
end

View File

@ -112,19 +112,25 @@ defmodule Farmbot.CeleryScript.RunTime.InstructionSet do
heap = get_heap_by_page_index(farm_proc, pc.page_address)
data = AST.unslice(heap, pc.heap_address)
data = case data.args.location do
%AST{kind: :identifier} ->
location = Resolver.resolve(farm_proc, pc, data.args.location.args.label)
%{data | args: %{data.args | location: location}}
_ -> data
end
data =
case data.args.location do
%AST{kind: :identifier} ->
location = Resolver.resolve(farm_proc, pc, data.args.location.args.label)
%{data | args: %{data.args | location: location}}
data = case data.args.offset do
%AST{kind: :identifier} ->
offset = Resolver.resolve(farm_proc, pc, data.args.offset.args.label)
%{data | args: %{data.args | offset: offset}}
_ -> data
end
_ ->
data
end
data =
case data.args.offset do
%AST{kind: :identifier} ->
offset = Resolver.resolve(farm_proc, pc, data.args.offset.args.label)
%{data | args: %{data.args | offset: offset}}
_ ->
data
end
case farm_proc.io_result do
# If we need to lookup a coordinate, do that.
@ -303,10 +309,12 @@ defmodule Farmbot.CeleryScript.RunTime.InstructionSet do
{:error, reason} ->
crash(farm_proc, reason)
:ok ->
farm_proc
|> clear_io_result()
|> set_pc_ptr(next_ptr)
_data ->
exception(farm_proc, "Bad execute_script implementation.")
end
@ -327,6 +335,7 @@ defmodule Farmbot.CeleryScript.RunTime.InstructionSet do
@spec return(FarmProc.t()) :: FarmProc.t()
defp return(%FarmProc{} = farm_proc) do
{value, farm_proc} = pop_rs(farm_proc)
farm_proc
|> set_pc_ptr(value)
|> set_status(:ok)
@ -336,6 +345,7 @@ defmodule Farmbot.CeleryScript.RunTime.InstructionSet do
defp next(%FarmProc{} = farm_proc) do
current_pc = get_pc_ptr(farm_proc)
next_ptr = get_next_address(farm_proc, current_pc)
farm_proc
|> set_pc_ptr(next_ptr)
|> set_status(:ok)
@ -349,6 +359,7 @@ defmodule Farmbot.CeleryScript.RunTime.InstructionSet do
is_null_address? = is_null_address?(addr)
return_stack_is_empty? = farm_proc.rs == []
cond do
is_null_address? && return_stack_is_empty? -> set_status(farm_proc, :done)
is_null_address? -> return(farm_proc)

View File

@ -2,7 +2,7 @@ defmodule Farmbot.CeleryScript.RunTime.ProcStorage do
@moduledoc """
Process wrapper around CircularList
"""
alias Farmbot.CeleryScript.RunTime.FarmProc
@opaque proc_storage :: pid

View File

@ -41,8 +41,7 @@ defmodule Farmbot.CeleryScript.RunTime.Resolver do
result = do_resolve(kind, farm_proc, pointer, label)
%Address{} = page = pointer.page_address
%Pointer{} =
new_pointer = Pointer.new(page, FarmProc.get_parent(farm_proc, pointer))
%Pointer{} = new_pointer = Pointer.new(page, FarmProc.get_parent(farm_proc, pointer))
if is_nil(result) do
search_tree(farm_proc, new_pointer, label)
@ -52,16 +51,14 @@ defmodule Farmbot.CeleryScript.RunTime.Resolver do
else
%Address{} = page = pointer.page_address
%Pointer{} =
new_pointer = Pointer.new(page, FarmProc.get_parent(farm_proc, pointer))
%Pointer{} = new_pointer = Pointer.new(page, FarmProc.get_parent(farm_proc, pointer))
search_tree(farm_proc, new_pointer, label)
end
end
def do_resolve(:sequence, farm_proc, pointer, label) do
locals_ptr =
FarmProc.get_cell_attr_as_pointer(farm_proc, pointer, :__locals)
locals_ptr = FarmProc.get_cell_attr_as_pointer(farm_proc, pointer, :__locals)
ast =
AST.unslice(

View File

@ -179,8 +179,7 @@ defmodule Farmbot.CeleryScript.AST.SlicerTest do
assert heap[addr(2)][@kind] == :"Elixir.Farmbot.CeleryScript.AST.Node.ROOT[0]"
assert heap[heap[addr(2)][@body]][@kind] ==
:"Elixir.Farmbot.CeleryScript.AST.Node.ROOT[0][0]"
assert heap[heap[addr(2)][@body]][@kind] == :"Elixir.Farmbot.CeleryScript.AST.Node.ROOT[0][0]"
assert heap[heap[addr(2)][@next]][@kind] == :"Elixir.Farmbot.CeleryScript.AST.Node.ROOT[1]"

View File

@ -64,8 +64,7 @@ defmodule Farmbot.CeleryScript.RunTime.FarmProcTest do
step2 = FarmProc.step(step1)
assert FarmProc.get_status(step2) == :crashed
assert FarmProc.get_pc_ptr(step2) ==
Pointer.null(FarmProc.get_zero_page(step1))
assert FarmProc.get_pc_ptr(step2) == Pointer.null(FarmProc.get_zero_page(step1))
end
test "io functions bad return values raise Farmbot.CeleryScript.RunTime.Error exception" do
@ -113,9 +112,7 @@ defmodule Farmbot.CeleryScript.RunTime.FarmProcTest do
Farmbot.CeleryScript.RunTime.TestSupport.Fixtures.heap()
)
assert FarmProc.is_null_address?(
Pointer.null(FarmProc.get_zero_page(farm_proc))
)
assert FarmProc.is_null_address?(Pointer.null(FarmProc.get_zero_page(farm_proc)))
assert FarmProc.is_null_address?(Address.null())
assert FarmProc.is_null_address?(Pointer.new(addr(0), addr(0)))
@ -396,8 +393,7 @@ defmodule Farmbot.CeleryScript.RunTime.FarmProcTest do
step7 = FarmProc.step(step6)
assert FarmProc.get_return_stack(step7) == []
assert FarmProc.get_pc_ptr(step7) ==
Pointer.null(FarmProc.get_zero_page(step7))
assert FarmProc.get_pc_ptr(step7) == Pointer.null(FarmProc.get_zero_page(step7))
end
test "raises when trying to step thru a crashed proc" do
@ -479,30 +475,26 @@ defmodule Farmbot.CeleryScript.RunTime.FarmProcTest do
# step into the sequence.
next = FarmProc.step(farm_proc)
assert FarmProc.get_pc_ptr(next) ==
Pointer.null(FarmProc.get_zero_page(next))
assert FarmProc.get_pc_ptr(next) == Pointer.null(FarmProc.get_zero_page(next))
assert FarmProc.get_return_stack(next) == []
# Each following step should still be stopped/paused.
next1 = FarmProc.step(next)
assert FarmProc.get_pc_ptr(next1) ==
Pointer.null(FarmProc.get_zero_page(next1))
assert FarmProc.get_pc_ptr(next1) == Pointer.null(FarmProc.get_zero_page(next1))
assert FarmProc.get_return_stack(next1) == []
next2 = FarmProc.step(next1)
assert FarmProc.get_pc_ptr(next2) ==
Pointer.null(FarmProc.get_zero_page(next2))
assert FarmProc.get_pc_ptr(next2) == Pointer.null(FarmProc.get_zero_page(next2))
assert FarmProc.get_return_stack(next2) == []
next3 = FarmProc.step(next2)
assert FarmProc.get_pc_ptr(next3) ==
Pointer.null(FarmProc.get_zero_page(next3))
assert FarmProc.get_pc_ptr(next3) == Pointer.null(FarmProc.get_zero_page(next3))
assert FarmProc.get_return_stack(next3) == []
end

View File

@ -19,8 +19,9 @@ defmodule Farmbot.CeleryScript.RunTime.FarmwareTest do
test "farmware" do
pid = self()
{:ok, agent} = Agent.start_link fn() -> 0 end
fun = fn(ast) ->
{:ok, agent} = Agent.start_link(fn -> 0 end)
fun = fn ast ->
case ast.kind do
:execute_script ->
case Agent.get_and_update(agent, fn state -> {state, state + 1} end) do
@ -28,20 +29,24 @@ defmodule Farmbot.CeleryScript.RunTime.FarmwareTest do
1 -> {:ok, ast(:wait, %{milliseconds: 456})}
2 -> :ok
end
:wait ->
send(pid, ast)
:ok
end
end
heap = @rpc_request
|> AST.decode()
|> AST.slice()
heap =
@rpc_request
|> AST.decode()
|> AST.slice()
%FarmProc{} = step0 = FarmProc.new(fun, addr(-1), heap)
complete = Enum.reduce(0..200, step0, fn(_, proc) ->
%FarmProc{} = wait_for_io(proc)
end)
complete =
Enum.reduce(0..200, step0, fn _, proc ->
%FarmProc{} = wait_for_io(proc)
end)
assert complete.status == :done

View File

@ -26,8 +26,7 @@ defmodule Farmbot.CeleryScript.RunTime.ResolverTest do
end
test "variable resolution" do
outer_json =
fetch_fixture("fixture/outer_sequence.json") |> AST.Slicer.run()
outer_json = fetch_fixture("fixture/outer_sequence.json") |> AST.Slicer.run()
farm_proc0 = FarmProc.new(io_fun(self()), addr(0), outer_json)
@ -41,7 +40,7 @@ defmodule Farmbot.CeleryScript.RunTime.ResolverTest do
assert_received %AST{
kind: :move_absolute,
args: %{
location: %AST{kind: :point, args: %{pointer_id: 456, pointer_type: "Plant"} },
location: %AST{kind: :point, args: %{pointer_id: 456, pointer_type: "Plant"}},
offset: %AST{kind: :coordinate, args: %{x: 0, y: 0, z: 0}},
speed: 100
}
@ -53,17 +52,17 @@ defmodule Farmbot.CeleryScript.RunTime.ResolverTest do
location: %AST{kind: :point, args: %{pointer_id: 123, pointer_type: "GenericPointer"}},
offset: %AST{kind: :coordinate, args: %{x: 0, y: 0, z: 0}},
speed: 100
},
}
}
assert_received %AST{
kind: :wait,
args: %{milliseconds: 1000},
args: %{milliseconds: 1000}
}
assert_received %AST{
kind: :wait,
args: %{milliseconds: 1050},
args: %{milliseconds: 1050}
}
end

View File

@ -88,8 +88,7 @@ defmodule Farmbot.CeleryScript.RunTimeTest do
RunTime.rpc_request(farmbot_celery_script, lock_ast, io_fun)
assert_receive :emergency_lock
unlock_ast =
ast(:rpc_request, %{label: name}, [ast(:emergency_unlock, %{})])
unlock_ast = ast(:rpc_request, %{label: name}, [ast(:emergency_unlock, %{})])
RunTime.rpc_request(farmbot_celery_script, unlock_ast, io_fun)
assert_receive :emergency_unlock
@ -117,11 +116,9 @@ defmodule Farmbot.CeleryScript.RunTimeTest do
label1 = "one"
label2 = "two"
ast1 =
ast(:rpc_request, %{label: label1}, [ast(:wait, %{milliseconds: to})])
ast1 = ast(:rpc_request, %{label: label1}, [ast(:wait, %{milliseconds: to})])
ast2 =
ast(:rpc_request, %{label: label2}, [ast(:wait, %{milliseconds: to})])
ast2 = ast(:rpc_request, %{label: label2}, [ast(:wait, %{milliseconds: to})])
cb = fn %{kind: :rpc_ok} = rpc_ok -> send(pid, rpc_ok) end
spawn_link(RunTime, :rpc_request, [farmbot_celery_script, ast1, cb])
@ -211,8 +208,7 @@ defmodule Farmbot.CeleryScript.RunTimeTest do
{:ok, farmbot_celery_script} = RunTime.start_link(opts, name)
ok_ast = ast(:sequence, %{id: 100}, [ast(:wait, %{milliseconds: 100})])
err_ast =
ast(:sequence, %{id: 101}, [ast(:send_message, %{message: "???"})])
err_ast = ast(:sequence, %{id: 101}, [ast(:send_message, %{message: "???"})])
cb = fn results -> send(pid, results) end
vm_pid = RunTime.sequence(farmbot_celery_script, ok_ast, 100, cb)

View File

@ -3,12 +3,16 @@
inputs: [
"*.{ex,exs}",
"{config,priv,test}/**/*.{ex,exs}",
"lib/bot_state/**/*.{ex,exs}",
"lib/bot_state_ng/**/*.{ex,exs}",
"lib/asset/**/*.{ex,exs}",
"lib/asset_workers/**/*.{ex,exs}",
"lib/farmware_runtime/**/*.{ex,exs}",
"lib/celery_script/**/*.{ex,exs}",
"lib/farmware_runtime.ex",
"lib/asset*.ex"
"lib/firmware/**/*.{ex,exs}",
"lib/asset*.ex",
"lib/log_storage/**/*.{ex,exs}"
],
subdirectories: ["priv/*/migrations"]
]

View File

@ -1,28 +1,27 @@
use Mix.Config
config :farmbot_core, Farmbot.AssetWorker.Farmbot.Asset.FarmEvent,
checkup_time_ms: 10_000
config :farmbot_core, Farmbot.AssetWorker.Farmbot.Asset.FarmEvent, checkup_time_ms: 10_000
config :farmbot_core, Farmbot.AssetWorker.Farmbot.Asset.FarmwareInstallation,
error_retry_time_ms: 30_000,
install_dir: "/tmp/farmware"
config :farmbot_core, Farmbot.AssetMonitor,
checkup_time_ms: 30_000
config :farmbot_core, Elixir.Farmbot.AssetWorker.Farmbot.Asset.PinBinding,
gpio_handler: Farmbot.PinBindingWorker.StubGPIOHandler,
error_retry_time_ms: 30_000
config :farmbot_core, Farmbot.AssetMonitor, checkup_time_ms: 30_000
config :farmbot_core,
expected_fw_versions: ["6.4.2.F", "6.4.2.R", "6.4.2.G"],
default_firmware_io_logs: false,
default_server: "https://my.farm.bot",
default_currently_on_beta:
String.contains?(to_string(:os.cmd('git rev-parse --abbrev-ref HEAD')), "beta"),
firmware_io_logs: false,
farm_event_debug_log: false
String.contains?(to_string(:os.cmd('git rev-parse --abbrev-ref HEAD')), "beta")
# Configure Farmbot Behaviours.
config :farmbot_core, :behaviour,
firmware_handler: Farmbot.Firmware.StubHandler,
leds_handler: Farmbot.Leds.StubHandler,
pin_binding_handler: Farmbot.PinBinding.StubHandler,
celery_script_io_layer: Farmbot.Core.CeleryScript.StubIOLayer,
json_parser: Farmbot.JSON.JasonParser

View File

@ -0,0 +1 @@

View File

@ -1,5 +1,5 @@
use Mix.Config
# config :logger,
# handle_otp_reports: true,
# handle_sasl_reports: true
config :logger,
handle_otp_reports: true,
handle_sasl_reports: true

View File

@ -2,11 +2,13 @@ defmodule Farmbot.Asset do
alias Farmbot.Asset.{
Repo,
Device,
DiagnosticDump,
FarmwareEnv,
FarmwareInstallation,
FarmEvent,
FbosConfig,
FirmwareConfig,
Peripheral,
PinBinding,
Regimen,
PersistentRegimen,
@ -106,6 +108,10 @@ defmodule Farmbot.Asset do
Repo.get_by!(Sequence, params)
end
def get_sequence(params) do
Repo.get_by(Sequence, params)
end
## End Sequence
## Begin FarmwareInstallation
@ -116,9 +122,42 @@ defmodule Farmbot.Asset do
|> Enum.find(fn %{package: pkg} -> pkg == package end)
end
## End FarmwareInstallation
## Begin FarmwareEnv
def list_farmware_env() do
Repo.all(FarmwareEnv)
end
## End FarmwareInstallation
def new_farmware_env(params) do
fwe =
if params["key"] || params[:key] do
Repo.get_by(FarmwareEnv, key: params["key"] || params[:key])
else
%FarmwareEnv{}
end
FarmwareEnv.changeset(fwe, params)
|> Repo.insert_or_update()
end
## End FarmwareEnv
## Begin Peripheral
def get_peripheral(args) do
Repo.get_by(Peripheral, args)
end
## End Peripheral
## Begin DiagnosticDump
def new_diagnostic_dump(params) do
DiagnosticDump.changeset(%DiagnosticDump{}, params)
|> Repo.insert()
end
## End DiagnosticDump
end

View File

@ -18,8 +18,10 @@ defmodule Elixir.Farmbot.Asset.FbosConfig do
field(:beta_opt_in, :boolean)
field(:disable_factory_reset, :boolean)
field(:firmware_hardware, :string)
field(:firmware_path, :string)
field(:firmware_input_log, :boolean)
field(:firmware_output_log, :boolean)
field(:firmware_debug_log, :boolean)
field(:network_not_found_timer, :integer)
field(:os_auto_update, :boolean)
field(:sequence_body_log, :boolean)
@ -36,8 +38,10 @@ defmodule Elixir.Farmbot.Asset.FbosConfig do
beta_opt_in: fbos_config.beta_opt_in,
disable_factory_reset: fbos_config.disable_factory_reset,
firmware_hardware: fbos_config.firmware_hardware,
firmware_path: fbos_config.firmware_path,
firmware_input_log: fbos_config.firmware_input_log,
firmware_output_log: fbos_config.firmware_output_log,
firmware_debug_log: fbos_config.firmware_debug_log,
network_not_found_timer: fbos_config.network_not_found_timer,
os_auto_update: fbos_config.os_auto_update,
sequence_body_log: fbos_config.sequence_body_log,
@ -55,8 +59,10 @@ defmodule Elixir.Farmbot.Asset.FbosConfig do
:beta_opt_in,
:disable_factory_reset,
:firmware_hardware,
:firmware_path,
:firmware_input_log,
:firmware_output_log,
:firmware_debug_log,
:network_not_found_timer,
:os_auto_update,
:sequence_body_log,

View File

@ -2,7 +2,6 @@ defmodule Farmbot.Asset.StorageAuth do
use Ecto.Schema
use Farmbot.Asset.Schema, path: "/api/storage_auth"
defmodule FormData do
use Ecto.Schema
import Ecto.Changeset
@ -21,7 +20,15 @@ defmodule Farmbot.Asset.StorageAuth do
def changeset(form_data, params \\ %{}) do
form_data
|> cast(params, [:key, :acl, :policy, :signature, :file, :"Content-Type", :GoogleAccessId])
|> validate_required([:key, :acl, :policy, :signature, :file, :"Content-Type", :GoogleAccessId])
|> validate_required([
:key,
:acl,
:policy,
:signature,
:file,
:"Content-Type",
:GoogleAccessId
])
end
end
@ -41,7 +48,7 @@ defmodule Farmbot.Asset.StorageAuth do
signature: storage_auth.form_data.signature,
file: storage_auth.form_data.file,
"Content-Type": storage_auth.form_data."Content-Type",
GoogleAccessId: storage_auth.form_data."GoogleAccessId",
GoogleAccessId: storage_auth.form_data."GoogleAccessId"
},
verb: storage_auth.verb,
url: storage_auth.url

View File

@ -5,11 +5,14 @@ defmodule Farmbot.Asset.Supervisor do
alias Farmbot.Asset.{
Repo,
PersistentRegimen,
Device,
FarmEvent,
FarmwareEnv,
FarmwareInstallation,
FbosConfig,
PinBinding,
Peripheral,
FarmwareInstallation
PersistentRegimen
}
def start_link(args) do
@ -19,11 +22,14 @@ defmodule Farmbot.Asset.Supervisor do
def init([]) do
children = [
Repo,
{AssetSupervisor, module: FbosConfig},
{AssetSupervisor, module: Device},
{AssetSupervisor, module: PersistentRegimen, preload: [:farm_event, :regimen]},
{AssetSupervisor, module: FarmEvent},
{AssetSupervisor, module: PinBinding},
{AssetSupervisor, module: Peripheral},
{AssetSupervisor, module: FarmwareInstallation},
{AssetSupervisor, module: FarmwareEnv},
AssetMonitor
]

View File

@ -4,8 +4,11 @@ defmodule Farmbot.AssetMonitor do
alias Farmbot.Asset.{
Repo,
Device,
FbosConfig,
FarmEvent,
FarmwareInstallation,
FarmwareEnv,
Peripheral,
PersistentRegimen,
PinBinding
@ -83,5 +86,15 @@ defmodule Farmbot.AssetMonitor do
end)
end
def order, do: [FarmEvent, Peripheral, PersistentRegimen, PinBinding, FarmwareInstallation]
def order,
do: [
Device,
FbosConfig,
FarmEvent,
# Peripheral,
PersistentRegimen,
PinBinding,
FarmwareInstallation,
FarmwareEnv
]
end

View File

@ -0,0 +1,18 @@
defimpl Farmbot.AssetWorker, for: Farmbot.Asset.Device do
alias Farmbot.Asset.Device
use GenServer
import Farmbot.Config, only: [update_config_value: 4]
def start_link(%Device{} = device) do
GenServer.start_link(__MODULE__, [%Device{} = device])
end
def init([%Device{} = device]) do
{:ok, %Device{} = device, 0}
end
def handle_info(:timeout, %Device{} = device) do
update_config_value(:string, "settings", "timezone", device.timezone)
{:noreply, device}
end
end

View File

@ -0,0 +1,17 @@
defimpl Farmbot.AssetWorker, for: Farmbot.Asset.FarmwareEnv do
alias Farmbot.Asset.FarmwareEnv
use GenServer
def start_link(%FarmwareEnv{} = env) do
GenServer.start_link(__MODULE__, env)
end
def init(%FarmwareEnv{} = env) do
{:ok, env, 0}
end
def handle_info(:timeout, %FarmwareEnv{key: key, value: value} = env) do
:ok = Farmbot.BotState.set_user_env(key, value)
{:noreply, env, :hibernate}
end
end

View File

@ -0,0 +1,68 @@
defimpl Farmbot.AssetWorker, for: Farmbot.Asset.FbosConfig do
use GenServer
require Logger
alias Farmbot.Asset.FbosConfig
import Farmbot.Config, only: [update_config_value: 4]
def start_link(%FbosConfig{} = fbos_config) do
GenServer.start_link(__MODULE__, [%FbosConfig{} = fbos_config])
end
def init([%FbosConfig{} = fbos_config]) do
{:ok, %FbosConfig{} = fbos_config, 0}
end
def handle_info(:timeout, %FbosConfig{} = fbos_config) do
maybe_reinit_firmware(fbos_config)
bool("arduino_debug_messages", fbos_config.arduino_debug_messages)
bool("auto_sync", fbos_config.auto_sync)
bool("beta_opt_in", fbos_config.beta_opt_in)
bool("disable_factory_reset", fbos_config.disable_factory_reset)
string("firmware_hardware", fbos_config.firmware_hardware)
bool("firmware_input_log", fbos_config.firmware_input_log)
bool("firmware_output_log", fbos_config.firmware_output_log)
float("network_not_found_timer", fbos_config.network_not_found_timer)
bool("os_auto_update", fbos_config.os_auto_update)
bool("sequence_body_log", fbos_config.sequence_body_log)
bool("sequence_complete_log", fbos_config.sequence_complete_log)
bool("sequence_init_log", fbos_config.sequence_init_log)
{:noreply, fbos_config}
end
defp bool(key, val) do
update_config_value(:bool, "settings", key, val)
:ok = Farmbot.BotState.set_config_value(key, val)
end
defp string(key, val) do
update_config_value(:string, "settings", key, val)
:ok = Farmbot.BotState.set_config_value(key, val)
end
defp float(_key, nil) do
:ok
end
defp float(key, val) do
update_config_value(:float, "settings", key, val / 1)
:ok = Farmbot.BotState.set_config_value(key, val)
end
defp maybe_reinit_firmware(%FbosConfig{firmware_hardware: nil}) do
:ok
end
defp maybe_reinit_firmware(%FbosConfig{firmware_path: nil}) do
:ok
end
defp maybe_reinit_firmware(%FbosConfig{}) do
alias Farmbot.Firmware
alias Farmbot.Core.FirmwareSupervisor
if is_nil(Process.whereis(Firmware)) do
Logger.warn("Starting Farmbot firmware")
FirmwareSupervisor.reinitialize()
end
end
end

View File

@ -14,19 +14,19 @@ defimpl Farmbot.AssetWorker, for: Farmbot.Asset.Peripheral do
end
def handle_info(:timeout, peripheral) do
# Farmbot.Logger.info 2, "Read peripheral: #{peripheral.label}"
Farmbot.Logger.busy(2, "Read peripheral: #{peripheral.label}")
CeleryScript.rpc_request(peripheral_to_rpc(peripheral), &handle_ast(&1, self()))
{:noreply, peripheral}
end
def handle_cast(%{kind: :rpc_ok}, peripheral) do
# Farmbot.Logger.success 2, "Read peripheral: #{peripheral.label} ok"
Farmbot.Logger.success(2, "Read peripheral: #{peripheral.label} ok")
{:stop, :normal, peripheral}
end
def handle_cast(%{kind: :rpc_error} = rpc, peripheral) do
# Farmbot.Logger.error 1, "Read peripheral: #{peripheral.label} error"
# IO.inspect(rpc, label: "error")
[%{args: %{message: reason}}] = rpc.body
Farmbot.Logger.error(1, "Read peripheral: #{peripheral.label} error: #{reason}")
{:noreply, peripheral, @retry_ms}
end

View File

@ -1,11 +1,155 @@
defimpl Farmbot.AssetWorker, for: Farmbot.Asset.PinBinding do
@gpio_handler Application.get_env(:farmbot_core, __MODULE__)[:gpio_handler]
@error_retry_time_ms Application.get_env(:farmbot_core, __MODULE__)[:error_retry_time_ms]
require Logger
require Farmbot.Logger
import Farmbot.CeleryScript.Utils
alias Farmbot.{
Core.CeleryScript,
Asset.PinBinding,
Asset.Sequence,
Asset
}
@gpio_handler ||
Mix.raise("""
config :farmbot_core, #{__MODULE__}, gpio_handler: MyModule
""")
@error_retry_time_ms ||
Mix.raise("""
config :farmbot_core, #{__MODULE__}, error_retry_time_ms: 30_000
""")
@typedoc "Opaque function that should be called upon a trigger"
@type trigger_fun :: (pid -> any)
@typedoc "Integer representing a GPIO on the target platform."
@type pin_number :: integer
@doc """
Start a GPIO Handler. Returns the same values as a GenServer start.
Should call `#{__MODULE__}.trigger/1` when a pin has been triggered.
"""
@callback start_link(pin_number, trigger_fun) :: GenServer.on_start()
use GenServer
def start_link(pin_binding) do
GenServer.start_link(__MODULE__, [pin_binding])
def start_link(%PinBinding{} = pin_binding) do
GenServer.start_link(__MODULE__, [%PinBinding{} = pin_binding])
end
def init([pin_binding]) do
{:ok, pin_binding}
# This function is opaque and should be considered private.
@doc false
def trigger(pid) do
GenServer.cast(pid, :trigger)
end
def init([%PinBinding{} = pin_binding]) do
{:ok, pin_binding, 0}
end
def handle_info(:timeout, %PinBinding{} = pin_binding) do
worker_pid = self()
case @gpio_handler.start_link(pin_binding.pin_num, fn -> trigger(worker_pid) end) do
{:ok, pid} when is_pid(pid) ->
Process.link(pid)
{:noreply, pin_binding}
{:error, {:already_started, pid}} ->
Process.link(pid)
{:noreply, pin_binding, :hibernate}
{:error, reason} ->
Logger.error("Failed to start PinBinding GPIO Handler: #{inspect(reason)}")
{:noreply, pin_binding, @error_retry_time_ms}
:ignore ->
Logger.info("Failed to start PinBinding GPIO Handler. Not retrying.")
{:noreply, pin_binding, :hibernate}
end
end
def handle_cast(:trigger, %PinBinding{special_action: nil} = pin_binding) do
case Asset.get_sequence(id: pin_binding.sequence_id) do
%Sequence{} = seq ->
pid = CeleryScript.sequence(seq, &handle_sequence_results(&1, pin_binding))
Process.link(pid)
{:noreply, pin_binding, :hibernate}
nil ->
Farmbot.Logger.error(1, "Failed to find assosiated Sequence for: #{pin_binding}")
{:noreply, pin_binding, :hibernate}
end
end
def handle_cast(:trigger, %PinBinding{special_action: "dump_info"} = pin_binding) do
ast(:rpc_request, %{label: "pin_binding.#{pin_binding.pin_num}"}, [ast(:dump_info, %{}, [])])
|> CeleryScript.rpc_request(&handle_rpc_request(&1, pin_binding))
end
def handle_cast(:trigger, %PinBinding{special_action: "emergency_lock"} = pin_binding) do
ast(:rpc_request, %{label: "pin_binding.#{pin_binding.pin_num}"}, [
ast(:emergency_lock, %{}, [])
])
|> CeleryScript.rpc_request(&handle_rpc_request(&1, pin_binding))
end
def handle_cast(:trigger, %PinBinding{special_action: "emergency_unlock"} = pin_binding) do
ast(:rpc_request, %{label: "pin_binding.#{pin_binding.pin_num}"}, [
ast(:emergency_unlock, %{}, [])
])
|> CeleryScript.rpc_request(&handle_rpc_request(&1, pin_binding))
end
def handle_cast(:trigger, %PinBinding{special_action: "power_off"} = pin_binding) do
ast(:rpc_request, %{label: "pin_binding.#{pin_binding.pin_num}"}, [ast(:power_off, %{}, [])])
|> CeleryScript.rpc_request(&handle_rpc_request(&1, pin_binding))
end
def handle_cast(:trigger, %PinBinding{special_action: "read_status"} = pin_binding) do
ast(:rpc_request, %{label: "pin_binding.#{pin_binding.pin_num}"}, [ast(:read_status, %{}, [])])
|> CeleryScript.rpc_request(&handle_rpc_request(&1, pin_binding))
end
def handle_cast(:trigger, %PinBinding{special_action: "reboot"} = pin_binding) do
ast(:rpc_request, %{label: "pin_binding.#{pin_binding.pin_num}"}, [ast(:reboot, %{}, [])])
|> CeleryScript.rpc_request(&handle_rpc_request(&1, pin_binding))
end
def handle_cast(:trigger, %PinBinding{special_action: "sync"} = pin_binding) do
ast(:rpc_request, %{label: "pin_binding.#{pin_binding.pin_num}"}, [ast(:sync, %{}, [])])
|> CeleryScript.rpc_request(&handle_rpc_request(&1, pin_binding))
end
def handle_cast(:trigger, %PinBinding{special_action: "take_photo"} = pin_binding) do
ast(:rpc_request, %{label: "pin_binding.#{pin_binding.pin_num}"}, [ast(:take_photo, %{}, [])])
|> CeleryScript.rpc_request(&handle_rpc_request(&1, pin_binding))
end
def handle_cast(:trigger, %PinBinding{} = pin_binding) do
Farmbot.Logger.error(1, "Unknown PinBinding: #{pin_binding}")
{:noreply, pin_binding, :hibernate}
end
def handle_sequence_results({:error, reason}, %PinBinding{} = pin_binding) do
Farmbot.Logger.error(1, "PinBinding #{pin_binding} failed to execute sequence: #{reason}")
end
def handle_sequence_results(_, _), do: :ok
def handle_rpc_request(
%{kind: :rpc_error, body: [%{args: %{message: m}}]},
%PinBinding{} = pin_binding
) do
Farmbot.Logger.error(1, "PinBinding: #{pin_binding} failed to execute special action: #{m}")
{:noreply, pin_binding, :hibernate}
end
def handle_rpc_request(_, %PinBinding{} = pin_binding),
do: {:noreply, pin_binding, :hibernate}
end

View File

@ -0,0 +1,31 @@
defmodule Farmbot.PinBindingWorker.StubGPIOHandler do
@moduledoc "Stub gpio handler for PinBindings"
@behaviour Farmbot.AssetWorker.Farmbot.Asset.PinBinding
require Logger
use GenServer
def start_link(pin_number, fun) do
GenServer.start_link(__MODULE__, [pin_number, fun], name: name(pin_number))
end
def terminate(reason, _state) do
Logger.warn("StubBindingHandler crash: #{inspect(reason)}")
end
def debug_trigger(pin_number) do
GenServer.call(name(pin_number), :debug_trigger)
end
def init([pin_number, fun]) do
Logger.info("StubBindingHandler init")
{:ok, %{pin_number: pin_number, fun: fun}}
end
def handle_call(:debug_trigger, _from, state) do
Logger.debug("DebugTrigger: #{state.pin_number}")
r = state.fun.()
{:reply, r, state}
end
def name(pin_number), do: :"#{__MODULE__}.#{pin_number}"
end

View File

@ -1,260 +1,296 @@
defmodule Farmbot.BotState do
@moduledoc "Central State accumulator."
alias Farmbot.BotState
alias BotState.{
McuParams,
LocationData,
Configuration,
InformationalSettings,
Pin
}
alias Farmbot.BotStateNG
use GenServer
defstruct [
mcu_params: struct(McuParams),
location_data: struct(LocationData),
configuration: struct(Configuration),
informational_settings: struct(InformationalSettings),
pins: %{},
process_info: %{farmwares: %{}},
gpio_registry: %{},
user_env: %{},
jobs: %{}
]
use GenStage
def download_progress_fun(name) do
alias Farmbot.BotState.JobProgress
require Farmbot.Logger
fn(bytes, total) ->
{do_send, prog} = cond do
# if the total is complete spit out the bytes,
# and put a status of complete.
total == :complete ->
Farmbot.Logger.success 3, "#{name} complete."
{true, %JobProgress.Bytes{bytes: bytes, status: :complete}}
# if we don't know the total just spit out the bytes.
total == nil ->
# debug_log "#{name} - #{bytes} bytes."
{rem(bytes, 10) == 0, %JobProgress.Bytes{bytes: bytes}}
# if the number of bytes == the total bytes,
# percentage side is complete.
(div(bytes, total)) == 1 ->
Farmbot.Logger.success 3, "#{name} complete."
{true, %JobProgress.Percent{percent: 100, status: :complete}}
# anything else is a percent.
true ->
percent = ((bytes / total) * 100) |> round()
# Logger.busy 3, "#{name} - #{bytes}/#{total} = #{percent}%"
{rem(percent, 10) == 0, %JobProgress.Percent{percent: percent}}
end
if do_send do
Farmbot.BotState.set_job_progress(name, prog)
else
:ok
end
end
@doc "Subscribe to BotState changes"
def subscribe(bot_state_server \\ __MODULE__) do
GenServer.call(bot_state_server, :subscribe)
end
@doc "Set job progress."
def set_job_progress(name, progress) do
GenServer.call(__MODULE__, {:set_job_progress, name, progress})
def set_job_progress(bot_state_server \\ __MODULE__, name, progress) do
GenServer.call(bot_state_server, {:set_job_progress, name, progress})
end
def clear_progress_fun(name) do
GenServer.call(__MODULE__, {:clear_progress_fun, name})
@doc "Set a configuration value"
def set_config_value(bot_state_server \\ __MODULE__, key, value) do
GenServer.call(bot_state_server, {:set_config_value, key, value})
end
@doc "Sets user_env value"
def set_user_env(bot_state_server \\ __MODULE__, key, value) do
GenServer.call(bot_state_server, {:set_user_env, key, value})
end
@doc "Sets the location_data.position"
def set_position(bot_state_server \\ __MODULE__, x, y, z) do
GenServer.call(bot_state_server, {:set_position, x, y, z})
end
@doc "Sets the location_data.encoders_scaled"
def set_encoders_scaled(bot_state_server \\ __MODULE__, x, y, z) do
GenServer.call(bot_state_server, {:set_encoders_scaled, x, y, z})
end
@doc "Sets pins.pin.value"
def set_pin_value(bot_state_server \\ __MODULE__, pin, value) do
GenServer.call(bot_state_server, {:set_pin_value, pin, value})
end
@doc "Sets the location_data.encoders_raw"
def set_encoders_raw(bot_state_server \\ __MODULE__, x, y, z) do
GenServer.call(bot_state_server, {:set_encoders_raw, x, y, z})
end
@doc "Sets mcu_params[param] = value"
def set_firmware_config(bot_state_server \\ __MODULE__, param, value) do
GenServer.call(bot_state_server, {:set_firmware_config, param, value})
end
@doc "Sets informational_settings.locked = true"
def set_firmware_locked(bot_state_server \\ __MODULE__) do
GenServer.call(bot_state_server, {:set_firmware_locked, true})
end
@doc "Sets informational_settings.locked = false"
def set_firmware_unlocked(bot_state_server \\ __MODULE__) do
GenServer.call(bot_state_server, {:set_firmware_locked, false})
end
def set_sync_status(bot_state_server \\ __MODULE__, s)
when s in ["syncing", "synced", "error"] do
GenServer.call(bot_state_server, {:set_sync_status, s})
end
@doc "Fetch the current state."
def fetch do
GenStage.call(__MODULE__, :fetch)
def fetch(bot_state_server \\ __MODULE__) do
GenServer.call(bot_state_server, :fetch)
end
def report_disk_usage(percent) when is_number(percent) do
GenStage.call(__MODULE__, {:report_disk_usage, percent})
def report_disk_usage(bot_state_server \\ __MODULE__, percent) do
GenServer.call(bot_state_server, {:report_disk_usage, percent})
end
def report_memory_usage(megabytes) when is_number(megabytes) do
GenStage.call(__MODULE__, {:report_memory_usage, megabytes})
def report_memory_usage(bot_state_server \\ __MODULE__, megabytes) do
GenServer.call(bot_state_server, {:report_memory_usage, megabytes})
end
def report_soc_temp(temp_celcius) when is_number(temp_celcius) do
GenStage.call(__MODULE__, {:report_soc_temp, temp_celcius})
def report_soc_temp(bot_state_server \\ __MODULE__, temp_celcius) do
GenServer.call(bot_state_server, {:report_soc_temp, temp_celcius})
end
def report_uptime(seconds) when is_number(seconds) do
GenStage.call(__MODULE__, {:report_uptime, seconds})
def report_uptime(bot_state_server \\ __MODULE__, seconds) do
GenServer.call(bot_state_server, {:report_uptime, seconds})
end
def report_wifi_level(level) when is_number(level) do
GenStage.call(__MODULE__, {:report_wifi_level, level})
def report_wifi_level(bot_state_server \\ __MODULE__, level) do
GenServer.call(bot_state_server, {:report_wifi_level, level})
end
@doc "Put FBOS into maintenance mode."
def enter_maintenance_mode do
GenStage.call(__MODULE__, :enter_maintenance_mode)
def enter_maintenance_mode(bot_state_server \\ __MODULE__) do
GenServer.call(bot_state_server, :enter_maintenance_mode)
end
@doc false
def start_link(args) do
GenStage.start_link(__MODULE__, args, [name: __MODULE__])
def start_link(args, opts \\ [name: __MODULE__]) do
GenServer.start_link(__MODULE__, args, opts)
end
@doc false
def init([]) do
Farmbot.Registry.subscribe()
send(self(), :get_initial_configuration)
send(self(), :get_initial_mcu_params)
# send(self(), :get_initial_user_env)
{:consumer, struct(BotState), [subscribe_to: [Farmbot.Firmware]]}
{:ok, %{tree: BotStateNG.new(), subscribers: []}}
end
@doc false
def handle_call(:subscribe, {pid, _} = _from, state) do
Process.link(pid)
{:reply, state.tree, %{state | subscribers: Enum.uniq([pid | state.subscribers])}}
end
def handle_call(:fetch, _from, state) do
new_state = handle_event({:informational_settings, %{cache_bust: :rand.uniform(1000)}}, state)
Farmbot.Registry.dispatch(__MODULE__, new_state)
{:reply, state, [], new_state}
{:reply, state.tree, state}
end
# TODO(Connor) - Fix this to use event system.
def handle_call({:set_job_progress, name, progress}, _from, state) do
jobs = Map.put(state.jobs, name, progress)
new_state = %{state | jobs: jobs}
Farmbot.Registry.dispatch(__MODULE__, new_state)
{:reply, :ok, [], new_state}
{reply, state} =
BotStateNG.set_job_progress(state.tree, name, Map.from_struct(progress))
|> dispatch_and_apply(state)
{:reply, reply, state}
end
# TODO(Connor) - Fix this to use event system.
def handle_call({:clear_progress_fun, name}, _from, state) do
jobs = Map.delete(state.jobs, name)
new_state = %{state | jobs: jobs}
Farmbot.Registry.dispatch(__MODULE__, new_state)
{:reply, :ok, [], new_state}
def handle_call({:set_config_value, key, value}, _from, state) do
change = %{configuration: %{key => value}}
{reply, state} =
BotStateNG.changeset(state.tree, change)
|> dispatch_and_apply(state)
{:reply, reply, state}
end
def handle_call({:set_user_env, key, value}, _from, state) do
{reply, state} =
BotStateNG.set_user_env(state.tree, key, value)
|> dispatch_and_apply(state)
{:reply, reply, state}
end
def handle_call({:set_position, x, y, z}, _from, state) do
change = %{location_data: %{position: %{x: x, y: y, z: z}}}
{reply, state} =
BotStateNG.changeset(state.tree, change)
|> dispatch_and_apply(state)
{:reply, reply, state}
end
def handle_call({:set_encoders_scaled, x, y, z}, _from, state) do
change = %{location_data: %{scaled_encoders: %{x: x, y: y, z: z}}}
{reply, state} =
BotStateNG.changeset(state.tree, change)
|> dispatch_and_apply(state)
{:reply, reply, state}
end
def handle_call({:set_encoders_raw, x, y, z}, _from, state) do
change = %{location_data: %{raw_encoders: %{x: x, y: y, z: z}}}
{reply, state} =
BotStateNG.changeset(state.tree, change)
|> dispatch_and_apply(state)
{:reply, reply, state}
end
def handle_call({:set_pin_value, pin, value}, _from, state) do
change = %{pins: %{pin => %{mode: -1, value: value}}}
{reply, state} =
BotStateNG.changeset(state.tree, change)
|> dispatch_and_apply(state)
{:reply, reply, state}
end
def handle_call({:set_firmware_config, param, value}, _from, state) do
change = %{mcu_params: %{param => value}}
{reply, state} =
BotStateNG.changeset(state.tree, change)
|> dispatch_and_apply(state)
{:reply, reply, state}
end
def handle_call({:set_firmware_locked, bool}, _from, state) do
change = %{informational_settings: %{locked: bool}}
{reply, state} =
BotStateNG.changeset(state.tree, change)
|> dispatch_and_apply(state)
{:reply, reply, state}
end
def handle_call({:set_sync_status, status}, _from, state) do
change = %{informational_settings: %{sync_status: status}}
{reply, state} =
BotStateNG.changeset(state.tree, change)
|> dispatch_and_apply(state)
{:reply, reply, state}
end
def handle_call({:report_disk_usage, percent}, _form, state) do
event = {:informational_settings, %{disk_usage: percent}}
new_state = handle_event(event, state)
Farmbot.Registry.dispatch(__MODULE__, new_state)
{:reply, :ok, [], new_state}
change = %{informational_settings: %{disk_usage: percent}}
{reply, state} =
BotStateNG.changeset(state.tree, change)
|> dispatch_and_apply(state)
{:reply, reply, state}
end
def handle_call({:memory_usage, megabytes}, _form, state) do
event = {:informational_settings, %{memory_usage: megabytes}}
new_state = handle_event(event, state)
Farmbot.Registry.dispatch(__MODULE__, new_state)
{:reply, :ok, [], new_state}
change = %{informational_settings: %{memory_usage: megabytes}}
{reply, state} =
BotStateNG.changeset(state.tree, change)
|> dispatch_and_apply(state)
{:reply, reply, state}
end
def handle_call({:report_soc_temp, temp}, _form, state) do
event = {:informational_settings, %{soc_temp: temp}}
new_state = handle_event(event, state)
Farmbot.Registry.dispatch(__MODULE__, new_state)
{:reply, :ok, [], new_state}
change = %{informational_settings: %{soc_temp: temp}}
{reply, state} =
BotStateNG.changeset(state.tree, change)
|> dispatch_and_apply(state)
{:reply, reply, state}
end
def handle_call({:uptime, seconds}, _form, state) do
event = {:informational_settings, %{uptime: seconds}}
new_state = handle_event(event, state)
Farmbot.Registry.dispatch(__MODULE__, new_state)
{:reply, :ok, [], new_state}
change = %{informational_settings: %{uptime: seconds}}
{reply, state} =
BotStateNG.changeset(state.tree, change)
|> dispatch_and_apply(state)
{:reply, reply, state}
end
def handle_call({:report_wifi_level, level}, _form, state) do
event = {:informational_settings, %{wifi_level: level}}
new_state = handle_event(event, state)
Farmbot.Registry.dispatch(__MODULE__, new_state)
{:reply, :ok, [], new_state}
change = %{informational_settings: %{wifi_level: level}}
{reply, state} =
BotStateNG.changeset(state.tree, change)
|> dispatch_and_apply(state)
{:reply, reply, state}
end
def handle_call(:enter_maintenance_mode, _form, state) do
event = {:informational_settings, %{sync_status: :maintenance}}
new_state = handle_event(event, state)
Farmbot.Registry.dispatch(__MODULE__, new_state)
{:reply, :ok, [], new_state}
change = %{informational_settings: %{sync_status: "maintenance"}}
{reply, state} =
BotStateNG.changeset(state.tree, change)
|> dispatch_and_apply(state)
{:reply, reply, state}
end
@doc false
def handle_info({Farmbot.Registry, {Farmbot.Config, {"settings", key, val}}}, state) do
event = {:settings, %{String.to_atom(key) => val}}
new_state = handle_event(event, state)
Farmbot.Registry.dispatch(__MODULE__, new_state)
{:noreply, [], new_state}
defp dispatch_and_apply(%Ecto.Changeset{changes: changes}, state) when map_size(changes) == 0 do
{:ok, state}
end
def handle_info({Farmbot.Registry, {_, {:sync_status, status}}}, state) do
event = {:informational_settings, %{sync_status: status}}
new_state = handle_event(event, state)
Farmbot.Registry.dispatch(__MODULE__, new_state)
{:noreply, [], new_state}
defp dispatch_and_apply(%Ecto.Changeset{valid?: true} = change, state) do
state = %{state | tree: Ecto.Changeset.apply_changes(change)}
state =
Enum.reduce(state.subscribers, state, fn pid, state ->
if Process.alive?(pid) do
send(pid, {__MODULE__, change})
state
else
Process.unlink(pid)
%{state | subscribers: List.delete(state.subscribers, pid)}
end
end)
{:ok, state}
end
def handle_info({Farmbot.Registry, _}, state), do: {:noreply, [], state}
def handle_info(:get_initial_configuration, state) do
full_config = Farmbot.Config.get_config_as_map()
settings = full_config["settings"]
new_state = Enum.reduce(settings, state, fn({key, val}, state) ->
event = {:settings, %{String.to_atom(key) => val}}
handle_event(event, state)
end)
Farmbot.Registry.dispatch(__MODULE__, new_state)
{:noreply, [], new_state}
end
def handle_info(:get_initial_mcu_params, state) do
full_config = Farmbot.Config.get_config_as_map()
settings = full_config["hardware_params"]
new_state = Enum.reduce(settings, state, fn({key, val}, state) ->
event = {:mcu_params, %{String.to_atom(key) => val}}
handle_event(event, state)
end)
Farmbot.Registry.dispatch(__MODULE__, new_state)
{:noreply, [], new_state}
end
@doc false
def handle_events(events, _from, state) do
state = Enum.reduce(events, state, &handle_event(&1, &2))
Farmbot.Registry.dispatch(__MODULE__, state)
{:noreply, [], state}
end
@doc false
def handle_event({:informational_settings, data}, state) do
new_data = Map.merge(state.informational_settings, data) |> Map.from_struct()
new_informational_settings = struct(InformationalSettings, new_data)
%{state | informational_settings: new_informational_settings}
end
def handle_event({:mcu_params, data}, state) do
new_data = Map.merge(state.mcu_params, data) |> Map.from_struct()
new_mcu_params = struct(McuParams, new_data)
%{state | mcu_params: new_mcu_params}
end
def handle_event({:location_data, data}, state) do
new_data = Map.merge(state.location_data, data) |> Map.from_struct()
new_location_data = struct(LocationData, new_data)
%{state | location_data: new_location_data}
end
def handle_event({:pins, data}, state) do
new_data = Enum.reduce(data, state.pins, fn({number, pin_state}, pins) ->
Map.put(pins, number, struct(Pin, pin_state))
end)
%{state | pins: new_data}
end
def handle_event({:settings, data}, state) do
new_data = Map.merge(state.configuration, data) |> Map.from_struct()
new_configuration = struct(Configuration, new_data)
%{state | configuration: new_configuration}
end
def handle_event(event, state) do
IO.inspect event, label: "unhandled event"
state
defp dispatch_and_apply(%Ecto.Changeset{valid?: false} = change, state) do
{{:error, change}, state}
end
end

View File

@ -1,33 +0,0 @@
defmodule Farmbot.BotState.Configuration do
@moduledoc false
defstruct [
timezone: nil,
sync_timeout_ms: nil,
sequence_init_log: nil,
sequence_complete_log: nil,
sequence_body_log: nil,
os_update_server_overwrite: nil,
os_auto_update: nil,
network_not_found_timer: nil,
log_amqp_connected: nil,
ignore_fbos_config: nil,
ignore_external_logs: nil,
fw_upgrade_migration: nil,
first_sync: nil,
first_party_farmware_url: nil,
first_party_farmware: nil,
first_boot: nil,
firmware_output_log: nil,
firmware_needs_first_sync: nil,
firmware_input_log: nil,
firmware_hardware: nil,
email_on_estop: nil,
disable_factory_reset: nil,
currently_on_beta: nil,
current_repo: nil,
beta_opt_in: nil,
auto_sync: nil,
arduino_debug_messages: nil,
api_migrated: nil
]
end

View File

@ -1,23 +0,0 @@
defmodule Farmbot.BotState.InformationalSettings do
@moduledoc false
import Farmbot.Project
defstruct [
target: target(),
env: env(),
node_name: node(),
controller_version: version(),
firmware_commit: arduino_commit(),
commit: commit(),
soc_temp: 0, # degrees celcius
wifi_level: nil, # decibels
uptime: 0, # seconds
memory_usage: 0, # megabytes
disk_usage: 0, # percent
firmware_version: nil,
sync_status: :sync_now,
last_status: :sync_now,
locked: nil,
cache_bust: nil,
busy: nil
]
end

View File

@ -8,23 +8,26 @@ defmodule Farmbot.BotState.JobProgress do
defmodule Percent do
@moduledoc "Percent job."
defstruct [status: :working, percent: 0, unit: :percent]
defstruct status: :working, percent: 0, unit: :percent, type: :ota, time: nil
defimpl Inspect, for: __MODULE__ do
def inspect(%{percent: percent}, _) do
"#Percent<#{percent}>"
end
end
@type t :: %__MODULE__{
status: Farmbot.BotState.JobProgress.status,
percent: integer,
unit: :percent
}
status: Farmbot.BotState.JobProgress.status(),
percent: integer,
unit: :percent,
type: :image | :ota,
time: DateTime.t()
}
end
defmodule Bytes do
@moduledoc "Bytes job."
defstruct [status: :working, bytes: 0, unit: :bytes]
defstruct status: :working, bytes: 0, unit: :bytes, type: :ota, time: nil
defimpl Inspect, for: __MODULE__ do
def inspect(%{bytes: bytes}, _) do
@ -33,11 +36,13 @@ defmodule Farmbot.BotState.JobProgress do
end
@type t :: %__MODULE__{
status: Farmbot.BotState.JobProgress.status,
bytes: integer,
unit: :bytes
}
status: Farmbot.BotState.JobProgress.status(),
bytes: integer,
unit: :bytes,
type: :image | :ota,
time: DateTime.t()
}
end
@type t :: Bytes.t | Percent.t
@type t :: Bytes.t() | Percent.t()
end

View File

@ -1,8 +0,0 @@
defmodule Farmbot.BotState.LocationData do
@moduledoc false
defstruct [
scaled_encoders: %{x: -1, y: -1, z: -1},
raw_encoders: %{x: -1, y: -1, z: -1},
position: %{x: -1, y: -1, z: -1}
]
end

View File

@ -1,97 +0,0 @@
defmodule Farmbot.BotState.McuParams do
@moduledoc false
defstruct [
:pin_guard_4_time_out,
:pin_guard_1_active_state,
:encoder_scaling_y,
:movement_invert_2_endpoints_x,
:movement_min_spd_y,
:pin_guard_2_time_out,
:movement_timeout_y,
:movement_home_at_boot_y,
:movement_home_spd_z,
:movement_invert_endpoints_z,
:pin_guard_1_pin_nr,
:movement_invert_endpoints_y,
:movement_max_spd_y,
:movement_home_up_y,
:encoder_missed_steps_decay_z,
:movement_home_spd_y,
:encoder_use_for_pos_x,
:movement_step_per_mm_x,
:movement_home_at_boot_z,
:movement_steps_acc_dec_z,
:pin_guard_5_pin_nr,
:movement_invert_motor_z,
:movement_max_spd_x,
:movement_enable_endpoints_y,
:movement_enable_endpoints_z,
:param_config_ok,
:movement_stop_at_home_x,
:movement_axis_nr_steps_y,
:pin_guard_1_time_out,
:movement_home_at_boot_x,
:pin_guard_2_pin_nr,
:encoder_scaling_z,
:param_e_stop_on_mov_err,
:encoder_enabled_x,
:pin_guard_2_active_state,
:encoder_missed_steps_decay_y,
:param_use_eeprom,
:movement_home_up_z,
:movement_enable_endpoints_x,
:movement_step_per_mm_y,
:pin_guard_3_pin_nr,
:param_mov_nr_retry,
:movement_stop_at_home_z,
:pin_guard_4_active_state,
:movement_steps_acc_dec_y,
:movement_home_spd_x,
:movement_keep_active_x,
:pin_guard_3_time_out,
:movement_keep_active_y,
:encoder_scaling_x,
:param_version,
:movement_invert_2_endpoints_z,
:encoder_missed_steps_decay_x,
:movement_timeout_z,
:encoder_missed_steps_max_z,
:movement_min_spd_z,
:encoder_enabled_y,
:encoder_type_y,
:movement_home_up_x,
:pin_guard_3_active_state,
:movement_invert_motor_x,
:movement_keep_active_z,
:movement_max_spd_z,
:movement_secondary_motor_invert_x,
:movement_stop_at_max_x,
:movement_steps_acc_dec_x,
:pin_guard_4_pin_nr,
:param_test,
:encoder_type_x,
:movement_invert_2_endpoints_y,
:encoder_invert_y,
:movement_axis_nr_steps_x,
:movement_stop_at_max_z,
:movement_invert_endpoints_x,
:encoder_invert_z,
:encoder_use_for_pos_z,
:pin_guard_5_active_state,
:movement_step_per_mm_z,
:encoder_enabled_z,
:movement_secondary_motor_x,
:pin_guard_5_time_out,
:movement_min_spd_x,
:encoder_type_z,
:movement_stop_at_max_y,
:encoder_use_for_pos_y,
:encoder_missed_steps_max_y,
:movement_timeout_x,
:movement_stop_at_home_y,
:movement_axis_nr_steps_z,
:encoder_invert_x,
:encoder_missed_steps_max_x,
:movement_invert_motor_y,
]
end

View File

@ -1,4 +0,0 @@
defmodule Farmbot.BotState.Pin do
@moduledoc false
defstruct [:mode, :value]
end

View File

@ -0,0 +1,94 @@
defmodule Farmbot.BotStateNG do
alias Farmbot.{
BotStateNG,
BotStateNG.McuParams,
BotStateNG.LocationData,
BotStateNG.InformationalSettings,
BotStateNG.ProcessInfo,
BotStateNG.Configuration
}
use Ecto.Schema
import Ecto.Changeset
@primary_key false
embedded_schema do
embeds_one(:mcu_params, McuParams, on_replace: :update)
embeds_one(:location_data, LocationData, on_replace: :update)
embeds_one(:informational_settings, InformationalSettings, on_replace: :update)
embeds_one(:process_info, ProcessInfo, on_replace: :update)
embeds_one(:configuration, Configuration, on_replace: :update)
field(:user_env, {:map, :string}, default: %{})
field(:pins, {:map, {:map, :integer}}, default: %{})
field(:jobs, {:map, :map}, default: %{})
end
def new do
%BotStateNG{}
|> changeset(%{})
|> put_embed(:mcu_params, McuParams.new())
|> put_embed(:location_data, LocationData.new())
|> put_embed(:informational_settings, InformationalSettings.new())
|> put_embed(:process_info, ProcessInfo.new())
|> put_embed(:configuration, Configuration.new())
|> apply_changes()
end
def changeset(bot_state, params \\ %{}) do
bot_state
|> cast(params, [:user_env, :pins, :jobs])
|> cast_embed(:mcu_params, [])
|> cast_embed(:location_data, [])
|> cast_embed(:informational_settings, [])
|> cast_embed(:process_info, [])
|> cast_embed(:configuration, [])
end
def view(bot_state) do
%{
mcu_params: McuParams.view(bot_state.mcu_params),
location_data: LocationData.view(bot_state.location_data),
informational_settings: InformationalSettings.view(bot_state.informational_settings),
process_info: ProcessInfo.view(bot_state.process_info),
configuration: Configuration.view(bot_state.configuration),
user_env: bot_state.user_env,
pins: bot_state.pins,
jobs: bot_state.jobs
}
end
@doc "Add or update a pin to state.pins."
def add_or_update_pin(state, number, mode, value) do
cs = changeset(state, %{})
new_pins =
cs
|> get_field(:pins)
|> Map.put(number, %{mode: mode, value: value})
put_change(cs, :pins, new_pins)
end
def set_user_env(state, key, value) do
cs = changeset(state, %{})
new_user_env =
cs
|> get_field(:user_env)
|> Map.put(key, value)
put_change(cs, :user_env, new_user_env)
end
def set_job_progress(state, name, progress) do
cs = changeset(state, %{})
new_jobs =
cs
|> get_field(:jobs)
|> Map.put(name, progress)
put_change(cs, :jobs, new_jobs)
end
end

View File

@ -0,0 +1,64 @@
defmodule Farmbot.BotStateNG.Configuration do
@moduledoc false
alias Farmbot.BotStateNG.Configuration
use Ecto.Schema
import Ecto.Changeset
@primary_key false
embedded_schema do
field(:arduino_debug_messages, :boolean)
field(:auto_sync, :boolean)
field(:beta_opt_in, :boolean)
field(:disable_factory_reset, :boolean)
field(:firmware_hardware, :string)
field(:firmware_input_log, :boolean)
field(:firmware_output_log, :boolean)
field(:network_not_found_timer, :integer)
field(:os_auto_update, :boolean)
field(:sequence_body_log, :boolean)
field(:sequence_complete_log, :boolean)
field(:sequence_init_log, :boolean)
end
def new do
%Configuration{}
|> changeset(%{})
|> apply_changes()
end
def view(configuration) do
%{
arduino_debug_messages: configuration.arduino_debug_messages,
auto_sync: configuration.auto_sync,
beta_opt_in: configuration.beta_opt_in,
disable_factory_reset: configuration.disable_factory_reset,
firmware_hardware: configuration.firmware_hardware,
firmware_input_log: configuration.firmware_input_log,
firmware_output_log: configuration.firmware_output_log,
network_not_found_timer: configuration.network_not_found_timer,
os_auto_update: configuration.os_auto_update,
sequence_body_log: configuration.sequence_body_log,
sequence_complete_log: configuration.sequence_complete_log,
sequence_init_log: configuration.sequence_init_log
}
end
def changeset(configuration, params \\ %{}) do
configuration
|> cast(params, [
:arduino_debug_messages,
:auto_sync,
:beta_opt_in,
:disable_factory_reset,
:firmware_hardware,
:firmware_input_log,
:firmware_output_log,
:network_not_found_timer,
:os_auto_update,
:sequence_body_log,
:sequence_complete_log,
:sequence_init_log
])
end
end

View File

@ -0,0 +1,81 @@
defmodule Farmbot.BotStateNG.InformationalSettings do
@moduledoc false
alias Farmbot.BotStateNG.InformationalSettings
use Ecto.Schema
import Ecto.Changeset
alias Farmbot.Project
@primary_key false
embedded_schema do
field(:target, :string, default: to_string(Project.target()))
field(:env, :string, default: to_string(Project.env()))
field(:controller_version, :string, default: Project.version())
field(:firmware_commit, :string, default: Project.arduino_commit())
field(:commit, :string, default: Project.commit())
field(:firmware_version, :string)
field(:node_name, :string)
field(:soc_temp, :integer)
field(:wifi_level, :integer)
field(:uptime, :integer)
field(:memory_usage, :integer)
field(:disk_usage, :integer)
field(:sync_status, :string, default: "sync_now")
field(:locked, :boolean, default: false)
field(:last_status, :string)
field(:cache_bust, :integer)
field(:busy, :boolean)
end
def new do
%InformationalSettings{}
|> changeset(%{})
|> apply_changes()
end
def view(informational_settings) do
%{
target: informational_settings.target,
env: informational_settings.env,
controller_version: informational_settings.controller_version,
firmware_commit: informational_settings.firmware_commit,
commit: informational_settings.commit,
firmware_version: informational_settings.firmware_version,
node_name: informational_settings.node_name,
soc_temp: informational_settings.soc_temp,
wifi_level: informational_settings.wifi_level,
uptime: informational_settings.uptime,
memory_usage: informational_settings.memory_usage,
disk_usage: informational_settings.disk_usage,
sync_status: informational_settings.sync_status,
locked: informational_settings.locked,
last_status: informational_settings.last_status,
cache_bust: informational_settings.cache_bust,
busy: informational_settings.busy
}
end
def changeset(informational_settings, params \\ %{}) do
informational_settings
|> cast(params, [
:target,
:env,
:controller_version,
:firmware_commit,
:commit,
:firmware_version,
:node_name,
:soc_temp,
:wifi_level,
:uptime,
:memory_usage,
:disk_usage,
:sync_status,
:locked,
:last_status,
:cache_bust,
:busy
])
end
end

View File

@ -0,0 +1,72 @@
defmodule Farmbot.BotStateNG.LocationData do
@moduledoc false
alias Farmbot.BotStateNG.LocationData
use Ecto.Schema
import Ecto.Changeset
@primary_key false
defmodule Vec3 do
@moduledoc false
use Ecto.Schema
import Ecto.Changeset
@primary_key false
embedded_schema do
field(:x, :float)
field(:y, :float)
field(:z, :float)
end
def new do
%__MODULE__{}
|> changeset(%{x: -1, y: -1, z: -1})
|> apply_changes()
end
def view(vec3) do
%{
x: vec3.x,
y: vec3.y,
z: vec3.z
}
end
def changeset(vec3, params \\ %{}) do
vec3
|> cast(params, [:x, :y, :z])
end
end
embedded_schema do
embeds_one(:scaled_encoders, Vec3, on_replace: :update)
embeds_one(:raw_encoders, Vec3, on_replace: :update)
embeds_one(:position, Vec3, on_replace: :update)
end
def new do
%LocationData{}
|> changeset(%{})
|> put_embed(:scaled_encoders, Vec3.new(), [])
|> put_embed(:raw_encoders, Vec3.new(), [])
|> put_embed(:position, Vec3.new(), [])
|> apply_changes()
end
def view(location_data) do
%{
scaled_encoders: Vec3.view(location_data.scaled_encoders),
raw_encoders: Vec3.view(location_data.raw_encoders),
position: Vec3.view(location_data.position)
}
end
def changeset(location_data, params \\ %{}) do
location_data
|> cast(params, [])
|> cast_embed(:scaled_encoders)
|> cast_embed(:raw_encoders)
|> cast_embed(:position)
end
end

View File

@ -0,0 +1,292 @@
defmodule Farmbot.BotStateNG.McuParams do
@moduledoc false
alias Farmbot.BotStateNG.McuParams
use Ecto.Schema
import Ecto.Changeset
@primary_key false
embedded_schema do
field(:pin_guard_4_time_out, :float)
field(:pin_guard_1_active_state, :float)
field(:encoder_scaling_y, :float)
field(:movement_invert_2_endpoints_x, :float)
field(:movement_min_spd_y, :float)
field(:pin_guard_2_time_out, :float)
field(:movement_timeout_y, :float)
field(:movement_home_at_boot_y, :float)
field(:movement_home_spd_z, :float)
field(:movement_invert_endpoints_z, :float)
field(:pin_guard_1_pin_nr, :float)
field(:movement_invert_endpoints_y, :float)
field(:movement_max_spd_y, :float)
field(:movement_home_up_y, :float)
field(:encoder_missed_steps_decay_z, :float)
field(:movement_home_spd_y, :float)
field(:encoder_use_for_pos_x, :float)
field(:movement_step_per_mm_x, :float)
field(:movement_home_at_boot_z, :float)
field(:movement_steps_acc_dec_z, :float)
field(:pin_guard_5_pin_nr, :float)
field(:movement_invert_motor_z, :float)
field(:movement_max_spd_x, :float)
field(:movement_enable_endpoints_y, :float)
field(:movement_enable_endpoints_z, :float)
field(:movement_stop_at_home_x, :float)
field(:movement_axis_nr_steps_y, :float)
field(:pin_guard_1_time_out, :float)
field(:movement_home_at_boot_x, :float)
field(:pin_guard_2_pin_nr, :float)
field(:encoder_scaling_z, :float)
field(:param_e_stop_on_mov_err, :float)
field(:encoder_enabled_x, :float)
field(:pin_guard_2_active_state, :float)
field(:encoder_missed_steps_decay_y, :float)
field(:movement_home_up_z, :float)
field(:movement_enable_endpoints_x, :float)
field(:movement_step_per_mm_y, :float)
field(:pin_guard_3_pin_nr, :float)
field(:param_mov_nr_retry, :float)
field(:movement_stop_at_home_z, :float)
field(:pin_guard_4_active_state, :float)
field(:movement_steps_acc_dec_y, :float)
field(:movement_home_spd_x, :float)
field(:movement_keep_active_x, :float)
field(:pin_guard_3_time_out, :float)
field(:movement_keep_active_y, :float)
field(:encoder_scaling_x, :float)
field(:movement_invert_2_endpoints_z, :float)
field(:encoder_missed_steps_decay_x, :float)
field(:movement_timeout_z, :float)
field(:encoder_missed_steps_max_z, :float)
field(:movement_min_spd_z, :float)
field(:encoder_enabled_y, :float)
field(:encoder_type_y, :float)
field(:movement_home_up_x, :float)
field(:pin_guard_3_active_state, :float)
field(:movement_invert_motor_x, :float)
field(:movement_keep_active_z, :float)
field(:movement_max_spd_z, :float)
field(:movement_secondary_motor_invert_x, :float)
field(:movement_stop_at_max_x, :float)
field(:movement_steps_acc_dec_x, :float)
field(:pin_guard_4_pin_nr, :float)
field(:encoder_type_x, :float)
field(:movement_invert_2_endpoints_y, :float)
field(:encoder_invert_y, :float)
field(:movement_axis_nr_steps_x, :float)
field(:movement_stop_at_max_z, :float)
field(:movement_invert_endpoints_x, :float)
field(:encoder_invert_z, :float)
field(:encoder_use_for_pos_z, :float)
field(:pin_guard_5_active_state, :float)
field(:movement_step_per_mm_z, :float)
field(:encoder_enabled_z, :float)
field(:movement_secondary_motor_x, :float)
field(:pin_guard_5_time_out, :float)
field(:movement_min_spd_x, :float)
field(:encoder_type_z, :float)
field(:movement_stop_at_max_y, :float)
field(:encoder_use_for_pos_y, :float)
field(:encoder_missed_steps_max_y, :float)
field(:movement_timeout_x, :float)
field(:movement_stop_at_home_y, :float)
field(:movement_axis_nr_steps_z, :float)
field(:encoder_invert_x, :float)
field(:encoder_missed_steps_max_x, :float)
field(:movement_invert_motor_y, :float)
end
def new() do
%McuParams{}
|> changeset(%{})
|> apply_changes()
end
def view(mcu_params) do
%{
pin_guard_4_time_out: mcu_params.pin_guard_4_time_out,
pin_guard_1_active_state: mcu_params.pin_guard_1_active_state,
encoder_scaling_y: mcu_params.encoder_scaling_y,
movement_invert_2_endpoints_x: mcu_params.movement_invert_2_endpoints_x,
movement_min_spd_y: mcu_params.movement_min_spd_y,
pin_guard_2_time_out: mcu_params.pin_guard_2_time_out,
movement_timeout_y: mcu_params.movement_timeout_y,
movement_home_at_boot_y: mcu_params.movement_home_at_boot_y,
movement_home_spd_z: mcu_params.movement_home_spd_z,
movement_invert_endpoints_z: mcu_params.movement_invert_endpoints_z,
pin_guard_1_pin_nr: mcu_params.pin_guard_1_pin_nr,
movement_invert_endpoints_y: mcu_params.movement_invert_endpoints_y,
movement_max_spd_y: mcu_params.movement_max_spd_y,
movement_home_up_y: mcu_params.movement_home_up_y,
encoder_missed_steps_decay_z: mcu_params.encoder_missed_steps_decay_z,
movement_home_spd_y: mcu_params.movement_home_spd_y,
encoder_use_for_pos_x: mcu_params.encoder_use_for_pos_x,
movement_step_per_mm_x: mcu_params.movement_step_per_mm_x,
movement_home_at_boot_z: mcu_params.movement_home_at_boot_z,
movement_steps_acc_dec_z: mcu_params.movement_steps_acc_dec_z,
pin_guard_5_pin_nr: mcu_params.pin_guard_5_pin_nr,
movement_invert_motor_z: mcu_params.movement_invert_motor_z,
movement_max_spd_x: mcu_params.movement_max_spd_x,
movement_enable_endpoints_y: mcu_params.movement_enable_endpoints_y,
movement_enable_endpoints_z: mcu_params.movement_enable_endpoints_z,
movement_stop_at_home_x: mcu_params.movement_stop_at_home_x,
movement_axis_nr_steps_y: mcu_params.movement_axis_nr_steps_y,
pin_guard_1_time_out: mcu_params.pin_guard_1_time_out,
movement_home_at_boot_x: mcu_params.movement_home_at_boot_x,
pin_guard_2_pin_nr: mcu_params.pin_guard_2_pin_nr,
encoder_scaling_z: mcu_params.encoder_scaling_z,
param_e_stop_on_mov_err: mcu_params.param_e_stop_on_mov_err,
encoder_enabled_x: mcu_params.encoder_enabled_x,
pin_guard_2_active_state: mcu_params.pin_guard_2_active_state,
encoder_missed_steps_decay_y: mcu_params.encoder_missed_steps_decay_y,
movement_home_up_z: mcu_params.movement_home_up_z,
movement_enable_endpoints_x: mcu_params.movement_enable_endpoints_x,
movement_step_per_mm_y: mcu_params.movement_step_per_mm_y,
pin_guard_3_pin_nr: mcu_params.pin_guard_3_pin_nr,
param_mov_nr_retry: mcu_params.param_mov_nr_retry,
movement_stop_at_home_z: mcu_params.movement_stop_at_home_z,
pin_guard_4_active_state: mcu_params.pin_guard_4_active_state,
movement_steps_acc_dec_y: mcu_params.movement_steps_acc_dec_y,
movement_home_spd_x: mcu_params.movement_home_spd_x,
movement_keep_active_x: mcu_params.movement_keep_active_x,
pin_guard_3_time_out: mcu_params.pin_guard_3_time_out,
movement_keep_active_y: mcu_params.movement_keep_active_y,
encoder_scaling_x: mcu_params.encoder_scaling_x,
movement_invert_2_endpoints_z: mcu_params.movement_invert_2_endpoints_z,
encoder_missed_steps_decay_x: mcu_params.encoder_missed_steps_decay_x,
movement_timeout_z: mcu_params.movement_timeout_z,
encoder_missed_steps_max_z: mcu_params.encoder_missed_steps_max_z,
movement_min_spd_z: mcu_params.movement_min_spd_z,
encoder_enabled_y: mcu_params.encoder_enabled_y,
encoder_type_y: mcu_params.encoder_type_y,
movement_home_up_x: mcu_params.movement_home_up_x,
pin_guard_3_active_state: mcu_params.pin_guard_3_active_state,
movement_invert_motor_x: mcu_params.movement_invert_motor_x,
movement_keep_active_z: mcu_params.movement_keep_active_z,
movement_max_spd_z: mcu_params.movement_max_spd_z,
movement_secondary_motor_invert_x: mcu_params.movement_secondary_motor_invert_x,
movement_stop_at_max_x: mcu_params.movement_stop_at_max_x,
movement_steps_acc_dec_x: mcu_params.movement_steps_acc_dec_x,
pin_guard_4_pin_nr: mcu_params.pin_guard_4_pin_nr,
encoder_type_x: mcu_params.encoder_type_x,
movement_invert_2_endpoints_y: mcu_params.movement_invert_2_endpoints_y,
encoder_invert_y: mcu_params.encoder_invert_y,
movement_axis_nr_steps_x: mcu_params.movement_axis_nr_steps_x,
movement_stop_at_max_z: mcu_params.movement_stop_at_max_z,
movement_invert_endpoints_x: mcu_params.movement_invert_endpoints_x,
encoder_invert_z: mcu_params.encoder_invert_z,
encoder_use_for_pos_z: mcu_params.encoder_use_for_pos_z,
pin_guard_5_active_state: mcu_params.pin_guard_5_active_state,
movement_step_per_mm_z: mcu_params.movement_step_per_mm_z,
encoder_enabled_z: mcu_params.encoder_enabled_z,
movement_secondary_motor_x: mcu_params.movement_secondary_motor_x,
pin_guard_5_time_out: mcu_params.pin_guard_5_time_out,
movement_min_spd_x: mcu_params.movement_min_spd_x,
encoder_type_z: mcu_params.encoder_type_z,
movement_stop_at_max_y: mcu_params.movement_stop_at_max_y,
encoder_use_for_pos_y: mcu_params.encoder_use_for_pos_y,
encoder_missed_steps_max_y: mcu_params.encoder_missed_steps_max_y,
movement_timeout_x: mcu_params.movement_timeout_x,
movement_stop_at_home_y: mcu_params.movement_stop_at_home_y,
movement_axis_nr_steps_z: mcu_params.movement_axis_nr_steps_z,
encoder_invert_x: mcu_params.encoder_invert_x,
encoder_missed_steps_max_x: mcu_params.encoder_missed_steps_max_x,
movement_invert_motor_y: mcu_params.movement_invert_motor_y
}
end
def changeset(mcu_params, params \\ %{}) do
mcu_params
|> cast(params, [
:pin_guard_4_time_out,
:pin_guard_1_active_state,
:encoder_scaling_y,
:movement_invert_2_endpoints_x,
:movement_min_spd_y,
:pin_guard_2_time_out,
:movement_timeout_y,
:movement_home_at_boot_y,
:movement_home_spd_z,
:movement_invert_endpoints_z,
:pin_guard_1_pin_nr,
:movement_invert_endpoints_y,
:movement_max_spd_y,
:movement_home_up_y,
:encoder_missed_steps_decay_z,
:movement_home_spd_y,
:encoder_use_for_pos_x,
:movement_step_per_mm_x,
:movement_home_at_boot_z,
:movement_steps_acc_dec_z,
:pin_guard_5_pin_nr,
:movement_invert_motor_z,
:movement_max_spd_x,
:movement_enable_endpoints_y,
:movement_enable_endpoints_z,
:movement_stop_at_home_x,
:movement_axis_nr_steps_y,
:pin_guard_1_time_out,
:movement_home_at_boot_x,
:pin_guard_2_pin_nr,
:encoder_scaling_z,
:param_e_stop_on_mov_err,
:encoder_enabled_x,
:pin_guard_2_active_state,
:encoder_missed_steps_decay_y,
:movement_home_up_z,
:movement_enable_endpoints_x,
:movement_step_per_mm_y,
:pin_guard_3_pin_nr,
:param_mov_nr_retry,
:movement_stop_at_home_z,
:pin_guard_4_active_state,
:movement_steps_acc_dec_y,
:movement_home_spd_x,
:movement_keep_active_x,
:pin_guard_3_time_out,
:movement_keep_active_y,
:encoder_scaling_x,
:movement_invert_2_endpoints_z,
:encoder_missed_steps_decay_x,
:movement_timeout_z,
:encoder_missed_steps_max_z,
:movement_min_spd_z,
:encoder_enabled_y,
:encoder_type_y,
:movement_home_up_x,
:pin_guard_3_active_state,
:movement_invert_motor_x,
:movement_keep_active_z,
:movement_max_spd_z,
:movement_secondary_motor_invert_x,
:movement_stop_at_max_x,
:movement_steps_acc_dec_x,
:pin_guard_4_pin_nr,
:encoder_type_x,
:movement_invert_2_endpoints_y,
:encoder_invert_y,
:movement_axis_nr_steps_x,
:movement_stop_at_max_z,
:movement_invert_endpoints_x,
:encoder_invert_z,
:encoder_use_for_pos_z,
:pin_guard_5_active_state,
:movement_step_per_mm_z,
:encoder_enabled_z,
:movement_secondary_motor_x,
:pin_guard_5_time_out,
:movement_min_spd_x,
:encoder_type_z,
:movement_stop_at_max_y,
:encoder_use_for_pos_y,
:encoder_missed_steps_max_y,
:movement_timeout_x,
:movement_stop_at_home_y,
:movement_axis_nr_steps_z,
:encoder_invert_x,
:encoder_missed_steps_max_x,
:movement_invert_motor_y
])
end
end

View File

@ -0,0 +1,27 @@
defmodule Farmbot.BotStateNG.ProcessInfo do
@moduledoc false
alias Farmbot.BotStateNG.ProcessInfo
use Ecto.Schema
import Ecto.Changeset
@primary_key false
embedded_schema do
field(:farmwares, {:map, {:map, :any}}, default: %{})
end
def new do
%ProcessInfo{}
|> changeset(%{})
|> apply_changes()
end
def view(process_info) do
%{farmwares: process_info.farmwares}
end
def changeset(process_info, params \\ %{}) do
process_info
|> cast(params, [:farmwares])
end
end

View File

@ -34,7 +34,7 @@ defmodule Farmbot.Core.CeleryScript.IOLayer do
@callback move_absolute(args, body) :: :ok | {:error, String.t()}
# Complex IO.
# @callbcak _if(args, body) :: {:ok, AST.t} | {:error, String.t}
# @callbcak _if(args, body) :: {:ok, AST.t()} | {:error, String.t()}
@callback execute(args, body) :: {:ok, AST.t()} | {:error, String.t()}
# Special IO.

View File

@ -82,7 +82,6 @@ defmodule Farmbot.Config do
|> apply(:"get_#{type}_value", [group_name, key_name])
|> Ecto.Changeset.change(value: value)
|> Repo.update!()
|> dispatch(group_name, key_name)
end
def update_config_value(type, _, _, _) do
@ -140,9 +139,4 @@ defmodule Farmbot.Config do
[group_id] = from(g in Group, where: g.group_name == ^group_name, select: g.id) |> Repo.all()
group_id
end
defp dispatch(%{value: value} = val, group, key) do
Farmbot.Registry.dispatch(__MODULE__, {group, key, value})
val
end
end

View File

@ -10,14 +10,13 @@ defmodule Farmbot.Core do
def init([]) do
children = [
{Farmbot.Registry, []},
{Farmbot.BotState, []},
{Farmbot.Logger.Supervisor, []},
{Farmbot.Config.Supervisor, []},
{Farmbot.Firmware.Supervisor, []},
{Farmbot.Asset.Supervisor, []},
{Farmbot.BotState, []},
{Farmbot.Core.FirmwareSupervisor, []},
{Farmbot.Core.CeleryScript.Supervisor, []},
]
Supervisor.init(children, [strategy: :one_for_one])
Supervisor.init(children, [strategy: :one_for_all])
end
end

View File

@ -2,7 +2,6 @@ defmodule Farmbot.FarmwareRuntime do
import Farmbot.AssetWorker.Farmbot.Asset.FarmwareInstallation, only: [install_dir: 1]
alias Farmbot.Asset
alias Asset.FarmwareInstallation.Manifest
alias Farmbot.FarmwareRuntime.PlugWrapper
import Farmbot.Config, only: [get_config_value: 3]

View File

@ -1,38 +0,0 @@
defmodule Farmbot.Firmware.Command do
@moduledoc """
Structured data to be sent to the Firmware.
"""
alias Farmbot.Firmware.{Command, Utils}
import Utils
defstruct [:fun, :args, :from, :status]
@doc "Add a status message to the Command."
def add_status(%Command{} = command, status) do
%{command | status: (command.status || []) ++ [status]}
end
def add_status(not_command, _), do: not_command
def format_args(%Farmbot.Firmware.Vec3{x: x, y: y, z: z}) do
"#{fmnt_float(x)}, #{fmnt_float(y)}, #{fmnt_float(z)}"
end
def format_args(arg) when is_atom(arg), do: to_string(arg)
def format_args(arg) when is_binary(arg), do: arg
def format_args(arg), do: inspect(arg)
end
defimpl Inspect, for: Farmbot.Firmware.Command do
def inspect(cmd, _) do
args = Enum.map(cmd.args, &Farmbot.Firmware.Command.format_args(&1))
"#{cmd.fun}(#{Enum.join(args, ", ")})"
end
end
defimpl String.Chars, for: Farmbot.Firmware.Command do
def to_string(cmd) do
args = Enum.map(cmd.args, &Farmbot.Firmware.Command.format_args(&1))
"#{cmd.fun}(#{Enum.join(args, ", ")})"
end
end

View File

@ -1,83 +0,0 @@
defmodule Farmbot.Firmware.CompletionLogs do
@moduledoc false
require Farmbot.Logger
import Farmbot.Config, only: [get_config_value: 3]
alias Farmbot.Firmware.Command
def maybe_log_complete(%Command{fun: :move_absolute, args: [pos | _]}, {:error, _reason}) do
unless get_config_value(:bool, "settings", "firmware_input_log") do
Farmbot.Logger.error 1, "Movement to #{inspect pos} failed."
end
end
def maybe_log_complete(%Command{fun: :move_absolute, args: [pos | _]} = current, _reply) do
unless get_config_value(:bool, "settings", "firmware_input_log") do
if current.status do
pos = Enum.reduce(current.status, pos, fn(status, pos) ->
case status do
{:report_axis_changed_x, new_pos} -> %{pos | x: new_pos}
{:report_axis_changed_y, new_pos} -> %{pos | y: new_pos}
{:report_axis_changed_z, new_pos} -> %{pos | z: new_pos}
_ -> pos
end
end)
Farmbot.Logger.success 1, "Movement to #{inspect pos} complete. (Stopped at end)"
else
Farmbot.Logger.success 1, "Movement to #{inspect pos} complete."
end
end
end
def maybe_log_complete(%Command{fun: :home}, {:error, _reason}) do
unless get_config_value(:bool, "settings", "firmware_input_log") do
Farmbot.Logger.error 1, "Movement to (0, 0, 0) failed."
end
end
def maybe_log_complete(%Command{fun: :home_all}, _reply) do
unless get_config_value(:bool, "settings", "firmware_input_log") do
Farmbot.Logger.success 1, "Movement to (0, 0, 0) complete."
end
end
def maybe_log_complete(_command, {:error, :report_axis_home_complete_x}) do
unless get_config_value(:bool, "settings", "firmware_input_log") do
Farmbot.Logger.success 2, "X Axis homing complete."
end
end
def maybe_log_complete(_command, {:error, :report_axis_home_complete_y}) do
unless get_config_value(:bool, "settings", "firmware_input_log") do
Farmbot.Logger.success 2, "Y Axis homing complete."
end
end
def maybe_log_complete(_command, {:error, :report_axis_home_complete_z}) do
unless get_config_value(:bool, "settings", "firmware_input_log") do
Farmbot.Logger.success 2, "Z Axis homing complete."
end
end
def maybe_log_complete(_command, {:error, :report_axis_timeout_x}) do
unless get_config_value(:bool, "settings", "firmware_input_log") do
Farmbot.Logger.error 2, "X Axis timeout."
end
end
def maybe_log_complete(_command, {:error, :report_axis_timeout_y}) do
unless get_config_value(:bool, "settings", "firmware_input_log") do
Farmbot.Logger.error 2, "Y Axis timeout."
end
end
def maybe_log_complete(_command, {:error, :report_axis_timeout_z}) do
unless get_config_value(:bool, "settings", "firmware_input_log") do
Farmbot.Logger.error 2, "Z Axis timeout."
end
end
def maybe_log_complete(_command, _result) do
# IO.puts "#{command} => #{inspect result}"
:ok
end
end

View File

@ -1,74 +1,58 @@
defmodule Farmbot.Firmware.EstopTimer do
defmodule Farmbot.Core.FirmwareEstopTimer do
@moduledoc """
Module responsible for timing emails about E stops.
Process that wraps a `Process.send_after/3` call.
When `:timeout` is received, a `fatal_email` log message will be
dispatched.
"""
use GenServer
require Farmbot.Logger
@msg "Farmbot has been E-Stopped for more than 10 minutes."
# Ten minutes.
@timer_ms 600_000
# fifteen seconds.
# @timer_ms 15000
@doc "Checks if the timer is active."
def timer_active? do
GenServer.call(__MODULE__, :timer_active?)
@ten_minutes_ms 60_0000
def start_timer(timer_server \\ __MODULE__) do
GenServer.call(timer_server, :start_timer)
end
@doc "Starts a new timer if one isn't started."
def start_timer do
GenServer.call(__MODULE__, :start_timer)
def cancel_timer(timer_server \\ __MODULE__) do
GenServer.call(timer_server, :cancel_timer)
end
@doc "Cancels a timer if it exists."
def cancel_timer do
GenServer.call(__MODULE__, :cancel_timer)
@doc """
optional args:
* `timeout_ms` - amount of milliseconds to run timer for
* `timeout_function` - function to call instead of logging
opts - GenServer.options()
"""
@spec start_link(Keyword.t(), GenServer.options()) :: GenServer.on_start()
def start_link(args, opts \\ [name: __MODULE__]) do
GenServer.start_link(__MODULE__, args, opts)
end
@doc false
def start_link(args) do
GenServer.start_link(__MODULE__, args, [name: __MODULE__])
end
@doc false
def init([]) do
{:ok, %{timer: nil, already_sent: false}}
end
def handle_call(:timer_active?, _, state) do
{:reply, is_timer_active?(state.timer), state}
def init(args) do
timeout_ms = Keyword.get(args, :timeout_ms, @ten_minutes_ms)
timeout_fun = Keyword.get(args, :timeout_function, &do_log/0)
state = %{timer: nil, timeout_ms: timeout_ms, timeout_function: timeout_fun}
{:ok, state, :hibernate}
end
def handle_call(:start_timer, _from, state) do
if is_timer_active?(state.timer) do
{:reply, :ok, state}
else
{:reply, :ok, %{state | timer: do_start_timer(self())}}
end
timer = Process.send_after(self(), :timeout, state.timeout_ms)
{:reply, timer, %{state | timer: timer}}
end
def handle_call(:cancel_timer, _from, state) do
if is_timer_active?(state.timer) do
Process.cancel_timer(state.timer)
end
{:reply, :ok, %{state | timer: nil, already_sent: false}}
state.timer && Process.cancel_timer(state.timer)
{:reply, state.timer, %{state | timer: nil}, :hibernate}
end
def handle_info(:timer, state) do
if state.already_sent do
{:noreply, %{state | timer: nil}}
else
Farmbot.Logger.warn 1, @msg, [channels: [:fatal_email]]
{:noreply, %{state | timer: nil, already_sent: true}}
end
def handle_info(:timeout, state) do
_ = apply(state.timeout_function, [])
{:noreply, %{state | timer: nil}, :hibernate}
end
defp is_timer_active?(timer) do
if timer, do: is_number(Process.read_timer(timer)), else: false
end
defp do_start_timer(pid) do
Process.send_after(pid, :timer, @timer_ms)
end
@doc false
def do_log, do: Farmbot.Logger.warn(1, @msg, channels: [:fatal_email])
end

View File

@ -1,656 +0,0 @@
defmodule Farmbot.Firmware do
@moduledoc "Allows communication with the firmware."
use GenStage
require Farmbot.Logger
alias Farmbot.Firmware.{Command, CompletionLogs, Vec3, EstopTimer, Utils}
import Utils
import Farmbot.Config,
only: [get_config_value: 3, update_config_value: 4, get_config_as_map: 0]
import CompletionLogs,
only: [maybe_log_complete: 2]
# If any command takes longer than this, exit.
@call_timeout 500_000
@doc "Move the bot to a position."
def move_absolute(%Vec3{} = vec3, x_spd, y_spd, z_spd) do
call = {:move_absolute, [vec3, x_spd, y_spd, z_spd]}
GenStage.call(__MODULE__, call, @call_timeout)
end
@doc "Calibrate an axis."
def calibrate(axis) when is_binary(axis) do
axis = String.to_atom(axis)
GenStage.call(__MODULE__, {:calibrate, [axis]}, @call_timeout)
end
@doc "Find home on an axis."
def find_home(axis) when is_binary(axis) do
axis = String.to_atom(axis)
GenStage.call(__MODULE__, {:find_home, [axis]}, @call_timeout)
end
@doc "Home every axis."
def home_all() do
GenStage.call(__MODULE__, {:home_all, []}, @call_timeout)
end
@doc "Home an axis."
def home(axis) when is_binary(axis) do
axis = String.to_atom(axis)
GenStage.call(__MODULE__, {:home, [axis]}, @call_timeout)
end
@doc "Manually set an axis's current position to zero."
def zero(axis) when is_binary(axis) do
axis = String.to_atom(axis)
GenStage.call(__MODULE__, {:zero, [axis]}, @call_timeout)
end
@doc """
Update a paramater.
For a list of paramaters see `Farmbot.Firmware.Gcode.Param`
"""
def update_param(param, val) do
GenStage.call(__MODULE__, {:update_param, [param, val]}, @call_timeout)
end
@doc false
def read_all_params do
GenStage.call(__MODULE__, {:read_all_params, []}, @call_timeout)
end
@doc """
Read a paramater.
For a list of paramaters see `Farmbot.Firmware.Gcode.Param`
"""
def read_param(param) do
GenStage.call(__MODULE__, {:read_param, [param]}, @call_timeout)
end
@doc "Emergency lock Farmbot."
def emergency_lock() do
GenStage.call(__MODULE__, {:emergency_lock, []}, @call_timeout)
end
@doc "Unlock Farmbot from Emergency state."
def emergency_unlock() do
GenStage.call(__MODULE__, {:emergency_unlock, []}, @call_timeout)
end
@doc "Set a pin mode (`:input` | `:output` | `:input_pullup`)"
def set_pin_mode(pin, mode) do
GenStage.call(__MODULE__, {:set_pin_mode, [pin, mode]}, @call_timeout)
end
@doc "Read a pin."
def read_pin(pin, mode) do
GenStage.call(__MODULE__, {:read_pin, [pin, mode]}, @call_timeout)
end
@doc "Write a pin."
def write_pin(pin, mode, value) do
GenStage.call(__MODULE__, {:write_pin, [pin, mode, value]}, @call_timeout)
end
@doc "Request version."
def request_software_version do
GenStage.call(__MODULE__, {:request_software_version, []}, @call_timeout)
end
@doc "Set angle of a servo pin."
def set_servo_angle(pin, value) do
GenStage.call(__MODULE__, {:set_servo_angle, [pin, value]}, @call_timeout)
end
@doc "Flag for all params reported."
def params_reported do
GenStage.call(__MODULE__, :params_reported)
end
def get_pin_value(pin_num) do
GenStage.call(__MODULE__, {:call, {:get_pin_value, pin_num}})
end
def get_current_position do
GenStage.call(__MODULE__, {:call, :get_current_position})
end
@doc "Start the firmware services."
def start_link(args) do
GenStage.start_link(__MODULE__, args, name: __MODULE__)
end
## GenStage
defmodule State do
@moduledoc false
defstruct [
handler: nil,
handler_mod: nil,
idle: false,
timer: nil,
location_data: %{
position: %{x: -1, y: -1, z: -1},
scaled_encoders: %{x: -1, y: -1, z: -1},
raw_encoders: %{x: -1, y: -1, z: -1},
},
pins: %{},
params: %{},
params_reported: false,
initialized: false,
initializing: false,
current: nil,
timeout_ms: 150_000,
queue: :queue.new(),
x_needs_home_on_boot: false,
y_needs_home_on_boot: false,
z_needs_home_on_boot: false
]
end
defp needs_home_on_boot do
x = (get_config_value(:float, "hardware_params", "movement_home_at_boot_x") || 0)
|> num_to_bool()
y = (get_config_value(:float, "hardware_params", "movement_home_at_boot_y") || 0)
|> num_to_bool()
z = (get_config_value(:float, "hardware_params", "movement_home_at_boot_z") || 0)
|> num_to_bool()
%{
x_needs_home_on_boot: x,
y_needs_home_on_boot: y,
z_needs_home_on_boot: z,
}
end
def init([]) do
handler_mod =
Application.get_env(:farmbot_core, :behaviour)[:firmware_handler] || raise("No fw handler.")
|> IO.inspect(label: "FW Handler")
case handler_mod.start_link() do
{:ok, handler} ->
initial = Map.merge(needs_home_on_boot(), %{handler: handler, handler_mod: handler_mod})
Process.flag(:trap_exit, true)
{
:producer_consumer,
struct(State, initial),
subscribe_to: [handler], dispatcher: GenStage.BroadcastDispatcher
}
:ignore ->
Farmbot.Logger.error 1, "Failed to initialize firmware. Falling back to stub implementation."
replace_firmware_handler(Farmbot.Firmware.StubHandler)
init([])
end
end
def terminate(reason, state) do
unless reason in [:normal, :shutdown] do
replace_firmware_handler(Farmbot.Firmware.StubHandler)
end
unless :queue.is_empty(state.queue) do
list = :queue.to_list(state.queue)
for cmd <- list do
:ok = do_reply(%{state | current: cmd}, {:error, "Firmware handler crash"})
end
end
end
def handle_info({:EXIT, _pid, :normal}, state) do
{:stop, :normal, state}
end
def handle_info({:EXIT, _, reason}, state) do
Farmbot.Logger.error 1, "Firmware handler: #{state.handler_mod} died: #{inspect reason}"
case state.handler_mod.start_link() do
{:ok, handler} ->
new_state = %{state | handler: handler}
{:noreply, [{:informational_settings, %{busy: false}}], %{new_state | initialized: false, idle: false}}
err -> {:stop, err, %{state | handler: false}}
end
end
# TODO(Connor): Put some sort of exit strategy here.
# If a firmware command keeps timingout/failing, Farmbot OS just keeps trying
# it. This can lead to infinate failures.
def handle_info({:command_timeout, %Command{} = timeout_command}, state) do
case state.current do
# Check if this timeout is actually talking about the current command.
^timeout_command = current ->
Farmbot.Logger.warn 1, "Timed out waiting for Firmware response. Retrying #{inspect current}) "
case apply(state.handler_mod, current.fun, [state.handler | current.args]) do
:ok ->
timer = start_timer(current, state.timeout_ms)
{:noreply, [], %{state | current: current, timer: timer}}
{:error, reason} = res when is_binary(reason) ->
do_reply(state, res)
{:noreply, [], %{state | current: nil, queue: :queue.new()}}
end
# If this timeout was not talking about the current command
%Command{} = current ->
Farmbot.Logger.debug 3, "Got stray timeout for command: #{inspect current}"
{:noreply, [], %{state | timer: nil}}
# If there is no current command, we got a different kind of stray.
# This is ok i guess.
nil -> {:noreply, [], %{state | timer: nil}}
end
end
def handle_call({:call, {:get_pin_value, pin_num}}, _from, state) do
{:reply, state.pins[pin_num], [], state}
end
def handle_call({:call, :get_current_position}, _from, state) do
{:reply, state.location_data.position, [], state}
end
def handle_call(:params_reported, _, state) do
{:reply, state.params_reported, [], state}
end
def handle_call({fun, args}, from, state = %{initialized: false})
when fun not in [:read_all_params, :update_param, :emergency_unlock, :emergency_lock, :request_software_version] do
next_current = struct(Command, from: from, fun: fun, args: args)
do_queue_cmd(next_current, state)
# {:reply, {:error, "uninitialized"}, [], state}
end
def handle_call({fun, args}, from, state) do
next_current = struct(Command, from: from, fun: fun, args: args)
current_current = state.current
cond do
fun == :emergency_lock ->
if current_current do
do_reply(state, {:error, "emergency_lock"})
end
do_begin_cmd(next_current, state, [])
match?(%Command{}, current_current) ->
do_queue_cmd(next_current, state)
is_nil(current_current) ->
do_begin_cmd(next_current, state, [])
end
end
defp do_begin_cmd(%Command{fun: fun, args: args, from: _from} = current, state, dispatch) do
case apply(state.handler_mod, fun, [state.handler | args]) do
:ok ->
timer = start_timer(current, state.timeout_ms)
if fun == :emergency_unlock do
new_dispatch = [{:informational_settings, %{busy: false, locked: false}} | dispatch]
{:noreply, new_dispatch, %{state | current: current, timer: timer}}
else
{:noreply, dispatch, %{state | current: current, timer: timer}}
end
{:error, reason} = res when is_binary(reason) ->
do_reply(%{state | current: current}, res)
{:noreply, dispatch, %{state | current: nil}}
end
end
defp do_queue_cmd(%Command{fun: _fun, args: _args, from: _from} = current, state) do
# Farmbot.Logger.busy 3, "FW Queuing: #{fun}: #{inspect from}"
new_q = :queue.in(current, state.queue)
{:noreply, [], %{state | queue: new_q}}
end
def handle_events(gcodes, _from, state) do
{diffs, state} = handle_gcodes(gcodes, state)
# if after handling the current buffer of gcodes,
# Try to start the next command in the queue if it exists.
if List.last(gcodes) == :idle && state.current == nil do
if state.initialized do
case :queue.out(state.queue) do
{{:value, next_current}, new_queue} ->
do_begin_cmd(next_current, %{state | queue: new_queue, current: next_current}, diffs)
{:empty, queue} -> # nothing to do if the queue is empty.
{:noreply, diffs, %{state | queue: queue}}
end
else
Farmbot.Logger.warn 1, "Fw not initialized yet"
{:noreply, diffs, state}
end
else
{:noreply, diffs, state}
end
end
defp handle_gcodes(codes, state, acc \\ [])
defp handle_gcodes([], state, acc), do: {Enum.reverse(acc), state}
defp handle_gcodes([code | rest], state, acc) do
case handle_gcode(code, state) do
{nil, new_state} -> handle_gcodes(rest, new_state, acc)
{key, diff, new_state} -> handle_gcodes(rest, new_state, [{key, diff} | acc])
end
end
defp handle_gcode({:debug_message, message}, state) do
if get_config_value(:bool, "settings", "arduino_debug_messages") do
Farmbot.Logger.debug 3, "Arduino debug message: #{message}"
end
{nil, state}
end
defp handle_gcode(code, state) when code in [:error, :invalid_command] do
maybe_cancel_timer(state.timer, state.current)
if state.current do
Farmbot.Logger.error 1, "Got #{code} while executing `#{inspect state.current}`."
do_reply(state, {:error, "Firmware error. See log."})
{nil, %{state | current: nil}}
else
{nil, state}
end
end
defp handle_gcode(:report_no_config, state) do
Farmbot.Logger.busy 1, "Initializing Firmware."
old = get_config_as_map()["hardware_params"]
spawn __MODULE__, :do_read_params, [Map.delete(old, "param_version")]
{nil, %{state | initialized: false, initializing: true}}
end
defp handle_gcode(:report_params_complete, state) do
Farmbot.Logger.success 1, "Firmware Initialized."
{nil, %{state | initialized: true, initializing: false}}
end
defp handle_gcode(:idle, %{initialized: false, initializing: false} = state) do
Farmbot.Logger.busy 3, "Firmware not initialized yet. Waiting for R88 message."
{nil, state}
end
defp handle_gcode(:idle, %{initialized: true, initializing: false, current: nil, z_needs_home_on_boot: true} = state) do
Farmbot.Logger.info 2, "Bootup homing Z axis"
spawn __MODULE__, :find_home, ["z"]
{nil, %{state | z_needs_home_on_boot: false}}
end
defp handle_gcode(:idle, %{initialized: true, initializing: false, current: nil, y_needs_home_on_boot: true} = state) do
Farmbot.Logger.info 2, "Bootup homing Y axis"
spawn __MODULE__, :find_home, ["y"]
{nil, %{state | y_needs_home_on_boot: false}}
end
defp handle_gcode(:idle, %{initialized: true, initializing: false, current: nil, x_needs_home_on_boot: true} = state) do
Farmbot.Logger.info 2, "Bootup homing X axis"
spawn __MODULE__, :find_home, ["x"]
{nil, %{state | x_needs_home_on_boot: false}}
end
defp handle_gcode(:idle, state) do
maybe_cancel_timer(state.timer, state.current)
if state.current do
Farmbot.Logger.warn 1, "Got idle while executing a command."
timer = start_timer(state.current, state.timeout_ms)
{nil, %{state | timer: timer}}
else
{:informational_settings, %{busy: false, locked: false}, %{state | idle: true}}
end
end
defp handle_gcode({:report_current_position, x, y, z}, state) do
position = %{position: %{x: x, y: y, z: z}}
new_state = %{state | location_data: Map.merge(state.location_data, position)}
{:location_data, position, new_state}
end
defp handle_gcode({:report_encoder_position_scaled, x, y, z}, state) do
scaled_encoders = %{scaled_encoders: %{x: x, y: y, z: z}}
new_state = %{state | location_data: Map.merge(state.location_data, scaled_encoders)}
{:location_data, scaled_encoders, new_state}
end
defp handle_gcode({:report_encoder_position_raw, x, y, z}, state) do
raw_encoders = %{raw_encoders: %{x: x, y: y, z: z}}
new_state = %{state | location_data: Map.merge(state.location_data, raw_encoders)}
{:location_data, raw_encoders, new_state}
end
defp handle_gcode({:report_end_stops, xa, xb, ya, yb, za, zb}, state) do
diff = %{end_stops: %{xa: xa, xb: xb, ya: ya, yb: yb, za: za, zb: zb}}
{:location_data, diff, state}
{nil, state}
end
defp handle_gcode({:report_pin_mode, pin, mode_atom}, state) do
# Farmbot.Logger.debug 3, "Got pin mode report: #{pin}: #{mode_atom}"
mode = extract_pin_mode(mode_atom)
case state.pins[pin] do
%{mode: _, value: _} = pin_map ->
{:pins, %{pin => %{pin_map | mode: mode}}, %{state | pins: %{state.pins | pin => %{pin_map | mode: mode}}}}
nil ->
{:pins, %{pin => %{mode: mode, value: -1}}, %{state | pins: Map.put(state.pins, pin, %{mode: mode, value: -1})}}
end
end
defp handle_gcode({:report_pin_value, pin, value}, state) do
# Farmbot.Logger.debug 3, "Got pin value report: #{pin}: #{value} old: #{inspect state.pins[pin]}"
case state.pins[pin] do
%{mode: _, value: _} = pin_map ->
{:pins, %{pin => %{pin_map | value: value}}, %{state | pins: %{state.pins | pin => %{pin_map | value: value}}}}
nil ->
{:pins, %{pin => %{mode: nil, value: value}}, %{state | pins: Map.put(state.pins, pin, %{mode: nil, value: value})}}
end
end
defp handle_gcode({:report_parameter_value, param, value}, state) when (value == -1) do
maybe_update_param_from_report(to_string(param), nil)
{:mcu_params, %{param => nil}, %{state | params: Map.put(state.params, param, value)}}
end
defp handle_gcode({:report_parameter_value, param, value}, state) when is_number(value) do
maybe_update_param_from_report(to_string(param), value)
{:mcu_params, %{param => value}, %{state | params: Map.put(state.params, param, value)}}
end
defp handle_gcode({:report_software_version, version}, state) do
hw = get_config_value(:string, "settings", "firmware_hardware")
Farmbot.Logger.debug 3, "Firmware reported software version: #{version} current firmware_hardware is: #{hw}"
case String.last(version) do
"F" ->
update_config_value(:string, "settings", "firmware_hardware", "farmduino")
"R" ->
update_config_value(:string, "settings", "firmware_hardware", "arduino")
"G" ->
update_config_value(:string, "settings", "firmware_hardware", "farmduino_k14")
_ -> :ok
end
{:informational_settings, %{firmware_version: version}, state}
end
defp handle_gcode(:report_axis_home_complete_x, state) do
{nil, state}
end
defp handle_gcode(:report_axis_home_complete_y, state) do
{nil, state}
end
defp handle_gcode(:report_axis_home_complete_z, state) do
{nil, %{state | timer: nil}}
end
defp handle_gcode(:report_axis_timeout_x, state) do
do_reply(state, {:error, "Axis X timeout"})
{nil, %{state | timer: nil}}
end
defp handle_gcode(:report_axis_timeout_y, state) do
do_reply(state, {:error, "Axis Y timeout"})
{nil, %{state | timer: nil}}
end
defp handle_gcode(:report_axis_timeout_z, state) do
do_reply(state, {:error, "Axis Z timeout"})
{nil, %{state | timer: nil}}
end
defp handle_gcode({:report_axis_changed_x, _new_x} = msg, state) do
new_current = Command.add_status(state.current, msg)
{nil, %{state | current: new_current}}
end
defp handle_gcode({:report_axis_changed_y, _new_y} = msg, state) do
new_current = Command.add_status(state.current, msg)
{nil, %{state | current: new_current}}
end
defp handle_gcode({:report_axis_changed_z, _new_z} = msg, state) do
new_current = Command.add_status(state.current, msg)
{nil, %{state | current: new_current}}
end
defp handle_gcode(:busy, state) do
maybe_cancel_timer(state.timer, state.current)
timer = if state.current do
start_timer(state.current, state.timeout_ms)
else
nil
end
{:informational_settings, %{busy: true}, %{state | idle: false, timer: timer}}
end
defp handle_gcode(:done, state) do
maybe_cancel_timer(state.timer, state.current)
if state.current do
do_reply(state, :ok)
{:informational_settings, %{busy: false}, %{state | current: nil}}
else
{:informational_settings, %{busy: false}, state}
end
end
defp handle_gcode(:report_emergency_lock, state) do
maybe_send_email()
state.current && do_reply(state, {:error, :emergency_lock})
{:informational_settings, %{locked: true}, %{state | current: nil}}
end
defp handle_gcode({:report_calibration, axis, status}, state) do
maybe_cancel_timer(state.timer, state.current)
Farmbot.Logger.busy 1, "Axis #{axis} calibration: #{status}"
{nil, state}
end
defp handle_gcode({:report_axis_calibration, param, val}, state) do
spawn __MODULE__, :report_calibration_callback, [5, param, val]
{nil, state}
end
defp handle_gcode(:noop, state) do
{nil, state}
end
defp handle_gcode(:received, state) do
{nil, state}
end
defp handle_gcode({:echo, _code}, state) do
{nil, state}
end
defp handle_gcode(code, state) do
Farmbot.Logger.warn(3, "unhandled code: #{inspect(code)}")
{nil, state}
end
defp maybe_cancel_timer(nil, _maybe_current_command) do
:ok
end
defp maybe_cancel_timer(timer, _maybe_current_command) do
Process.read_timer(timer) && Process.cancel_timer(timer)
:ok
end
defp start_timer(%Command{} = command, timeout) do
Process.send_after(self(), {:command_timeout, command}, timeout)
end
defp maybe_update_param_from_report(param, val) when is_binary(param) do
real_val = if val, do: (val / 1), else: nil
# Farmbot.Logger.debug 3, "Firmware reported #{param} => #{val || -1}"
update_config_value(:float, "hardware_params", to_string(param), real_val)
end
@doc false
def do_read_params(old) when is_map(old) do
for {key, float_val} <- old do
cond do
(float_val == -1) -> :ok
is_nil(float_val) -> :ok
is_number(float_val) ->
val = round(float_val)
:ok = update_param(:"#{key}", val)
end
end
:ok = update_param(:param_use_eeprom, 0)
:ok = update_param(:param_config_ok, 1)
read_all_params()
:ok = request_software_version()
end
@doc false
def report_calibration_callback(tries, param, value)
def report_calibration_callback(0, _param, _value) do
:ok
end
def report_calibration_callback(tries, param, val) do
case Farmbot.Firmware.update_param(param, val) do
:ok ->
str_param = to_string(param)
case get_config_value(:float, "hardware_params", str_param) do
^val ->
Farmbot.Logger.success 1, "Calibrated #{param}: #{val}"
# SettingsSync.upload_fw_kv(str_param, val)
raise("fixme")
:ok
_ -> report_calibration_callback(tries - 1, param, val)
end
{:error, reason} when is_binary(reason) ->
Farmbot.Logger.error 1, "Failed to set #{param}: #{val} (#{inspect reason})"
report_calibration_callback(tries - 1, param, val)
end
end
defp do_reply(state, reply) do
maybe_cancel_timer(state.timer, state.current)
maybe_log_complete(state.current, reply)
case state.current do
%Command{fun: :emergency_unlock, from: from} ->
# i really don't want this to be here..
EstopTimer.cancel_timer()
:ok = GenServer.reply from, reply
%Command{fun: :emergency_lock, from: from} ->
:ok = GenServer.reply from, {:error, "Emergency Lock"}
%Command{fun: _fun, from: from} ->
# Farmbot.Logger.success 3, "FW Replying: #{fun}: #{inspect from}"
:ok = GenServer.reply from, reply
nil ->
Farmbot.Logger.error 1, "FW Nothing to send reply: #{inspect reply} to!."
:error
end
end
defp maybe_send_email do
if get_config_value(:bool, "settings", "email_on_estop") do
if !EstopTimer.timer_active? do
EstopTimer.start_timer()
end
end
end
end

View File

@ -0,0 +1,164 @@
defmodule Farmbot.Core.FirmwareSideEffects do
@moduledoc "Handles firmware data and syncing it with BotState."
@behaviour Farmbot.Firmware.SideEffects
alias Farmbot.Core.FirmwareEstopTimer
require Logger
require Farmbot.Logger
def handle_position(x: x, y: y, z: z) do
:ok = Farmbot.BotState.set_position(x, y, z)
end
def handle_position_change([{_axis, _value}]) do
:noop
end
def handle_axis_state([{_axis, _state}]) do
:noop
end
def handle_calibration_state([{_axis, _state}]) do
:noop
end
def handle_encoders_scaled(x: x, y: y, z: z) do
:ok = Farmbot.BotState.set_encoders_scaled(x, y, z)
end
def handle_encoders_raw(x: x, y: y, z: z) do
:ok = Farmbot.BotState.set_encoders_raw(x, y, z)
end
def handle_paramater_value([{param, value}]) do
:ok = Farmbot.BotState.set_firmware_config(param, value)
end
def handle_pin_value(p: pin, v: value) do
:ok = Farmbot.BotState.set_pin_value(pin, value)
end
def handle_software_version([_version]) do
:noop
end
def handle_end_stops(_) do
:noop
end
def handle_emergency_lock() do
_ = FirmwareEstopTimer.start_timer()
:ok = Farmbot.BotState.set_firmware_locked()
end
def handle_emergency_unlock() do
_ = FirmwareEstopTimer.cancel_timer()
:ok = Farmbot.BotState.set_firmware_unlocked()
end
def handle_input_gcode(code) do
should_log? = Farmbot.Asset.fbos_config().firmware_input_log
should_log? && Farmbot.Logger.debug(3, inspect(code))
end
def handle_output_gcode(code) do
should_log? = Farmbot.Asset.fbos_config().firmware_output_log
should_log? && Farmbot.Logger.debug(3, inspect(code))
end
def handle_debug_message([message]) do
should_log? = Farmbot.Asset.fbos_config().firmware_debug_log
should_log? && Farmbot.Logger.debug(3, "Arduino debug message: " <> message)
end
def load_params do
conf = Farmbot.Asset.firmware_config()
Map.take(conf, [
:param_e_stop_on_mov_err,
:param_mov_nr_retry,
:movement_timeout_x,
:movement_timeout_y,
:movement_timeout_z,
:movement_keep_active_x,
:movement_keep_active_y,
:movement_keep_active_z,
:movement_home_at_boot_x,
:movement_home_at_boot_y,
:movement_home_at_boot_z,
:movement_invert_endpoints_x,
:movement_invert_endpoints_y,
:movement_invert_endpoints_z,
:movement_enable_endpoints_x,
:movement_enable_endpoints_y,
:movement_enable_endpoints_z,
:movement_invert_motor_x,
:movement_invert_motor_y,
:movement_invert_motor_z,
:movement_secondary_motor_x,
:movement_secondary_motor_invert_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_home_up_x,
:movement_home_up_y,
:movement_home_up_z,
:movement_step_per_mm_x,
:movement_step_per_mm_y,
:movement_step_per_mm_z,
:movement_min_spd_x,
:movement_min_spd_y,
:movement_min_spd_z,
:movement_home_spd_x,
:movement_home_spd_y,
:movement_home_spd_z,
:movement_max_spd_x,
:movement_max_spd_y,
:movement_max_spd_z,
:encoder_enabled_x,
:encoder_enabled_y,
:encoder_enabled_z,
:encoder_type_x,
:encoder_type_y,
:encoder_type_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_missed_steps_decay_x,
:encoder_missed_steps_decay_y,
:encoder_missed_steps_decay_z,
:encoder_use_for_pos_x,
:encoder_use_for_pos_y,
:encoder_use_for_pos_z,
:encoder_invert_x,
:encoder_invert_y,
:encoder_invert_z,
:movement_axis_nr_steps_x,
:movement_axis_nr_steps_y,
:movement_axis_nr_steps_z,
:movement_stop_at_max_x,
:movement_stop_at_max_y,
:movement_stop_at_max_z,
:pin_guard_1_pin_nr,
:pin_guard_1_time_out,
:pin_guard_1_active_state,
:pin_guard_2_pin_nr,
:pin_guard_2_time_out,
:pin_guard_2_active_state,
:pin_guard_3_pin_nr,
:pin_guard_3_time_out,
:pin_guard_3_active_state,
:pin_guard_4_pin_nr,
:pin_guard_4_time_out,
:pin_guard_4_active_state,
:pin_guard_5_pin_nr,
:pin_guard_5_time_out,
:pin_guard_5_active_state
])
end
end

View File

@ -0,0 +1,51 @@
defmodule Farmbot.Core.FirmwareSupervisor do
use Supervisor
alias Farmbot.Asset
def start_link(args) do
Supervisor.start_link(__MODULE__, args, name: __MODULE__)
end
def reinitialize do
_ = Supervisor.terminate_child(Farmbot.Core, __MODULE__)
Supervisor.restart_child(Farmbot.Core, __MODULE__)
end
def stub do
Asset.fbos_config()
|> Asset.FbosConfig.changeset(%{firmware_path: "stub"})
|> Asset.Repo.insert_or_update()
end
def init([]) do
fbos_config = Asset.fbos_config()
children =
firmware_children(fbos_config) ++
[
Farmbot.Core.FirmwareEstopTimer
]
Supervisor.init(children, strategy: :one_for_all)
end
def firmware_children(%Asset.FbosConfig{firmware_hardware: nil}), do: []
def firmware_children(%Asset.FbosConfig{firmware_path: "stub"}) do
[
{Farmbot.Firmware,
transport: Farmbot.Firmware.StubTransport, side_effects: Farmbot.Core.FirmwareSideEffects}
]
end
def firmware_children(%Asset.FbosConfig{firmware_path: nil}), do: []
def firmware_children(%Asset.FbosConfig{} = fbos_config) do
[
{Farmbot.Firmware,
device: fbos_config.firmware_path,
transport: Farmbot.Firmware.UARTTransport,
side_effects: Farmbot.Core.FirmwareSideEffects}
]
end
end

View File

@ -1,284 +0,0 @@
defmodule Farmbot.Firmware.Gcode.Param do
@moduledoc "Firmware paramaters."
@doc "Turn a number into a param, or a param into a number."
@spec parse_param(integer | t) :: t | integer
def parse_param(0), do: :param_version
def parse_param(1), do: :param_test
def parse_param(2), do: :param_config_ok
def parse_param(3), do: :param_use_eeprom
def parse_param(4), do: :param_e_stop_on_mov_err
def parse_param(5), do: :param_mov_nr_retry
def parse_param(11), do: :movement_timeout_x
def parse_param(12), do: :movement_timeout_y
def parse_param(13), do: :movement_timeout_z
def parse_param(15), do: :movement_keep_active_x
def parse_param(16), do: :movement_keep_active_y
def parse_param(17), do: :movement_keep_active_z
def parse_param(18), do: :movement_home_at_boot_x
def parse_param(19), do: :movement_home_at_boot_y
def parse_param(20), do: :movement_home_at_boot_z
def parse_param(21), do: :movement_invert_endpoints_x
def parse_param(22), do: :movement_invert_endpoints_y
def parse_param(23), do: :movement_invert_endpoints_z
def parse_param(25), do: :movement_enable_endpoints_x
def parse_param(26), do: :movement_enable_endpoints_y
def parse_param(27), do: :movement_enable_endpoints_z
def parse_param(31), do: :movement_invert_motor_x
def parse_param(32), do: :movement_invert_motor_y
def parse_param(33), do: :movement_invert_motor_z
def parse_param(36), do: :movement_secondary_motor_x
def parse_param(37), do: :movement_secondary_motor_invert_x
def parse_param(41), do: :movement_steps_acc_dec_x
def parse_param(42), do: :movement_steps_acc_dec_y
def parse_param(43), do: :movement_steps_acc_dec_z
def parse_param(45), do: :movement_stop_at_home_x
def parse_param(46), do: :movement_stop_at_home_y
def parse_param(47), do: :movement_stop_at_home_z
def parse_param(51), do: :movement_home_up_x
def parse_param(52), do: :movement_home_up_y
def parse_param(53), do: :movement_home_up_z
def parse_param(55), do: :movement_step_per_mm_x
def parse_param(56), do: :movement_step_per_mm_y
def parse_param(57), do: :movement_step_per_mm_z
def parse_param(61), do: :movement_min_spd_x
def parse_param(62), do: :movement_min_spd_y
def parse_param(63), do: :movement_min_spd_z
def parse_param(65), do: :movement_home_spd_x
def parse_param(66), do: :movement_home_spd_y
def parse_param(67), do: :movement_home_spd_z
def parse_param(71), do: :movement_max_spd_x
def parse_param(72), do: :movement_max_spd_y
def parse_param(73), do: :movement_max_spd_z
def parse_param(75), do: :movement_invert_2_endpoints_x
def parse_param(76), do: :movement_invert_2_endpoints_y
def parse_param(77), do: :movement_invert_2_endpoints_z
def parse_param(101), do: :encoder_enabled_x
def parse_param(102), do: :encoder_enabled_y
def parse_param(103), do: :encoder_enabled_z
def parse_param(105), do: :encoder_type_x
def parse_param(106), do: :encoder_type_y
def parse_param(107), do: :encoder_type_z
def parse_param(111), do: :encoder_missed_steps_max_x
def parse_param(112), do: :encoder_missed_steps_max_y
def parse_param(113), do: :encoder_missed_steps_max_z
def parse_param(115), do: :encoder_scaling_x
def parse_param(116), do: :encoder_scaling_y
def parse_param(117), do: :encoder_scaling_z
def parse_param(121), do: :encoder_missed_steps_decay_x
def parse_param(122), do: :encoder_missed_steps_decay_y
def parse_param(123), do: :encoder_missed_steps_decay_z
def parse_param(125), do: :encoder_use_for_pos_x
def parse_param(126), do: :encoder_use_for_pos_y
def parse_param(127), do: :encoder_use_for_pos_z
def parse_param(131), do: :encoder_invert_x
def parse_param(132), do: :encoder_invert_y
def parse_param(133), do: :encoder_invert_z
def parse_param(141), do: :movement_axis_nr_steps_x
def parse_param(142), do: :movement_axis_nr_steps_y
def parse_param(143), do: :movement_axis_nr_steps_z
def parse_param(145), do: :movement_stop_at_max_x
def parse_param(146), do: :movement_stop_at_max_y
def parse_param(147), do: :movement_stop_at_max_z
def parse_param(201), do: :pin_guard_1_pin_nr
def parse_param(202), do: :pin_guard_1_time_out
def parse_param(203), do: :pin_guard_1_active_state
def parse_param(205), do: :pin_guard_2_pin_nr
def parse_param(206), do: :pin_guard_2_time_out
def parse_param(207), do: :pin_guard_2_active_state
def parse_param(211), do: :pin_guard_3_pin_nr
def parse_param(212), do: :pin_guard_3_time_out
def parse_param(213), do: :pin_guard_3_active_state
def parse_param(215), do: :pin_guard_4_pin_nr
def parse_param(216), do: :pin_guard_4_time_out
def parse_param(217), do: :pin_guard_4_active_state
def parse_param(221), do: :pin_guard_5_pin_nr
def parse_param(222), do: :pin_guard_5_time_out
def parse_param(223), do: :pin_guard_5_active_state
def parse_param(:param_version), do: 0
def parse_param(:param_test), do: 1
def parse_param(:param_config_ok), do: 2
def parse_param(:param_use_eeprom), do: 3
def parse_param(:param_e_stop_on_mov_err), do: 4
def parse_param(:param_mov_nr_retry), do: 5
def parse_param(:movement_timeout_x), do: 11
def parse_param(:movement_timeout_y), do: 12
def parse_param(:movement_timeout_z), do: 13
def parse_param(:movement_keep_active_x), do: 15
def parse_param(:movement_keep_active_y), do: 16
def parse_param(:movement_keep_active_z), do: 17
def parse_param(:movement_home_at_boot_x), do: 18
def parse_param(:movement_home_at_boot_y), do: 19
def parse_param(:movement_home_at_boot_z), do: 20
def parse_param(:movement_invert_endpoints_x), do: 21
def parse_param(:movement_invert_endpoints_y), do: 22
def parse_param(:movement_invert_endpoints_z), do: 23
def parse_param(:movement_enable_endpoints_x), do: 25
def parse_param(:movement_enable_endpoints_y), do: 26
def parse_param(:movement_enable_endpoints_z), do: 27
def parse_param(:movement_invert_motor_x), do: 31
def parse_param(:movement_invert_motor_y), do: 32
def parse_param(:movement_invert_motor_z), do: 33
def parse_param(:movement_secondary_motor_x), do: 36
def parse_param(:movement_secondary_motor_invert_x), do: 37
def parse_param(:movement_steps_acc_dec_x), do: 41
def parse_param(:movement_steps_acc_dec_y), do: 42
def parse_param(:movement_steps_acc_dec_z), do: 43
def parse_param(:movement_stop_at_home_x), do: 45
def parse_param(:movement_stop_at_home_y), do: 46
def parse_param(:movement_stop_at_home_z), do: 47
def parse_param(:movement_home_up_x), do: 51
def parse_param(:movement_home_up_y), do: 52
def parse_param(:movement_home_up_z), do: 53
def parse_param(:movement_step_per_mm_x), do: 55
def parse_param(:movement_step_per_mm_y), do: 56
def parse_param(:movement_step_per_mm_z), do: 57
def parse_param(:movement_min_spd_x), do: 61
def parse_param(:movement_min_spd_y), do: 62
def parse_param(:movement_min_spd_z), do: 63
def parse_param(:movement_home_spd_x), do: 65
def parse_param(:movement_home_spd_y), do: 66
def parse_param(:movement_home_spd_z), do: 67
def parse_param(:movement_max_spd_x), do: 71
def parse_param(:movement_max_spd_y), do: 72
def parse_param(:movement_max_spd_z), do: 73
def parse_param(:movement_invert_2_endpoints_x), do: 75
def parse_param(:movement_invert_2_endpoints_y), do: 76
def parse_param(:movement_invert_2_endpoints_z), do: 77
def parse_param(:encoder_enabled_x), do: 101
def parse_param(:encoder_enabled_y), do: 102
def parse_param(:encoder_enabled_z), do: 103
def parse_param(:encoder_type_x), do: 105
def parse_param(:encoder_type_y), do: 106
def parse_param(:encoder_type_z), do: 107
def parse_param(:encoder_missed_steps_max_x), do: 111
def parse_param(:encoder_missed_steps_max_y), do: 112
def parse_param(:encoder_missed_steps_max_z), do: 113
def parse_param(:encoder_scaling_x), do: 115
def parse_param(:encoder_scaling_y), do: 116
def parse_param(:encoder_scaling_z), do: 117
def parse_param(:encoder_missed_steps_decay_x), do: 121
def parse_param(:encoder_missed_steps_decay_y), do: 122
def parse_param(:encoder_missed_steps_decay_z), do: 123
def parse_param(:encoder_use_for_pos_x), do: 125
def parse_param(:encoder_use_for_pos_y), do: 126
def parse_param(:encoder_use_for_pos_z), do: 127
def parse_param(:encoder_invert_x), do: 131
def parse_param(:encoder_invert_y), do: 132
def parse_param(:encoder_invert_z), do: 133
def parse_param(:movement_axis_nr_steps_x), do: 141
def parse_param(:movement_axis_nr_steps_y), do: 142
def parse_param(:movement_axis_nr_steps_z), do: 143
def parse_param(:movement_stop_at_max_x), do: 145
def parse_param(:movement_stop_at_max_y), do: 146
def parse_param(:movement_stop_at_max_z), do: 147
def parse_param(:pin_guard_1_pin_nr), do: 201
def parse_param(:pin_guard_1_time_out), do: 202
def parse_param(:pin_guard_1_active_state), do: 203
def parse_param(:pin_guard_2_pin_nr), do: 205
def parse_param(:pin_guard_2_time_out), do: 206
def parse_param(:pin_guard_2_active_state), do: 207
def parse_param(:pin_guard_3_pin_nr), do: 211
def parse_param(:pin_guard_3_time_out), do: 212
def parse_param(:pin_guard_3_active_state), do: 213
def parse_param(:pin_guard_4_pin_nr), do: 215
def parse_param(:pin_guard_4_time_out), do: 216
def parse_param(:pin_guard_4_active_state), do: 217
def parse_param(:pin_guard_5_pin_nr), do: 221
def parse_param(:pin_guard_5_time_out), do: 222
def parse_param(:pin_guard_5_active_state), do: 223
@typedoc "Human readable param name."
@type t :: :param_config_ok |
:param_use_eeprom |
:param_e_stop_on_mov_err |
:param_mov_nr_retry |
:movement_timeout_x |
:movement_timeout_y |
:movement_timeout_z |
:movement_keep_active_x |
:movement_keep_active_y |
:movement_keep_active_z |
:movement_home_at_boot_x |
:movement_home_at_boot_y |
:movement_home_at_boot_z |
:movement_invert_endpoints_x |
:movement_invert_endpoints_y |
:movement_invert_endpoints_z |
:movement_enable_endpoints_x |
:movement_enable_endpoints_y |
:movement_enable_endpoints_z |
:movement_invert_motor_x |
:movement_invert_motor_y |
:movement_invert_motor_z |
:movement_secondary_motor_x |
:movement_secondary_motor_invert_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_home_up_x |
:movement_home_up_y |
:movement_home_up_z |
:movement_step_per_mm_x |
:movement_step_per_mm_y |
:movement_step_per_mm_z |
:movement_min_spd_x |
:movement_min_spd_y |
:movement_min_spd_z |
:movement_home_spd_x |
:movement_home_spd_y |
:movement_home_spd_z |
:movement_max_spd_x |
:movement_max_spd_y |
:movement_max_spd_z |
:movement_invert_2_endpoints_x |
:movement_invert_2_endpoints_y |
:movement_invert_2_endpoints_z |
:encoder_enabled_x |
:encoder_enabled_y |
:encoder_enabled_z |
:encoder_type_x |
:encoder_type_y |
:encoder_type_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_missed_steps_decay_x |
:encoder_missed_steps_decay_y |
:encoder_missed_steps_decay_z |
:encoder_use_for_pos_x |
:encoder_use_for_pos_y |
:encoder_use_for_pos_z |
:encoder_invert_x |
:encoder_invert_y |
:encoder_invert_z |
:movement_axis_nr_steps_x |
:movement_axis_nr_steps_y |
:movement_axis_nr_steps_z |
:movement_stop_at_max_x |
:movement_stop_at_max_y |
:movement_stop_at_max_z |
:pin_guard_1_pin_nr |
:pin_guard_1_time_out |
:pin_guard_1_active_state |
:pin_guard_2_pin_nr |
:pin_guard_2_time_out |
:pin_guard_2_active_state |
:pin_guard_3_pin_nr |
:pin_guard_3_time_out |
:pin_guard_3_active_state |
:pin_guard_4_pin_nr |
:pin_guard_4_time_out |
:pin_guard_4_active_state |
:pin_guard_5_pin_nr |
:pin_guard_5_time_out |
:pin_guard_5_active_state
end

View File

@ -1,192 +0,0 @@
defmodule Farmbot.Firmware.Gcode.Parser do
@moduledoc """
Parses [farmbot-arduino-firmware](https://github.com/farmbot/farmbot-arduino-firmware) G-Codes.
"""
import Farmbot.Firmware.Gcode.Param
@spec parse_code(binary) :: {binary | nil, tuple | atom}
# Status codes.
@doc "Parse a code to an Elixir consumable message."
def parse_code("R00 Q" <> tag), do: {tag, :idle}
def parse_code("R01 Q" <> tag), do: {tag, :received}
def parse_code("R02 Q" <> tag), do: {tag, :done}
def parse_code("R03 Q" <> tag), do: {tag, :error}
def parse_code("R04 Q" <> tag), do: {tag, :busy}
def parse_code("R05" <> _r), do: {nil, :noop}
def parse_code("R06 " <> r), do: parse_report_calibration(r)
def parse_code("R07 " <> _), do: {nil, :noop}
def parse_code("R08 " <> echo),
do: {:echo, {:echo, String.replace(echo, "\r", "")}}
def parse_code("R09 " <> tag), do: {tag, :invalid_command}
# Report axis homing.
def parse_code("R11 " <> tag), do: {tag, :report_axis_home_complete_x}
def parse_code("R12 " <> tag), do: {tag, :report_axis_home_complete_y}
def parse_code("R13 " <> tag), do: {tag, :report_axis_home_complete_z}
def parse_code("R15 " <> data) do
["X" <> num_str, "Q" <> tag] = String.split(data, " ")
{tag, {:report_axis_changed_x, String.to_integer(num_str)}}
end
def parse_code("R16 " <> data) do
["Y" <> num_str, "Q" <> tag] = String.split(data, " ")
{tag, {:report_axis_changed_y, String.to_integer(num_str)}}
end
def parse_code("R17 " <> data) do
["Z" <> num_str, "Q" <> tag] = String.split(data, " ")
{tag, {:report_axis_changed_z, String.to_integer(num_str)}}
end
# Param report.
def parse_code("R20 Q" <> tag), do: {tag, :report_params_complete}
def parse_code("R21 " <> params), do: parse_pvq(params, :report_parameter_value)
def parse_code("R23 " <> params), do: parse_report_axis_calibration(params)
def parse_code("R31 " <> params), do: parse_pvq(params, :report_status_value)
def parse_code("R41 " <> params), do: parse_pvq(params, :report_pin_value)
#TODO(connor) - remove one of these variants. (With or without Q) at some point.
def parse_code("R71 Q" <> tag), do: {tag, :report_axis_timeout_x}
def parse_code("R72 Q" <> tag), do: {tag, :report_axis_timeout_y}
def parse_code("R73 Q" <> tag), do: {tag, :report_axis_timeout_z}
def parse_code("R71"), do: {nil, :report_axis_timeout_x}
def parse_code("R72"), do: {nil, :report_axis_timeout_y}
def parse_code("R73"), do: {nil, :report_axis_timeout_z}
# Report Position.
def parse_code("R81 " <> params), do: parse_end_stops(params)
def parse_code("R82 " <> p), do: report_xyz(p, :report_current_position)
def parse_code("R83 " <> v), do: parse_version(v)
def parse_code("R84 " <> p), do: report_xyz(p, :report_encoder_position_scaled)
def parse_code("R85 " <> p), do: report_xyz(p, :report_encoder_position_raw)
def parse_code("R87 Q" <> q), do: {q, :report_emergency_lock}
def parse_code("R88 Q" <> q), do: {q, :report_no_config}
def parse_code("R99 " <> message) do
{nil, {:debug_message, message}}
end
def parse_code(code) do
{:unhandled_gcode, code}
end
@spec parse_report_calibration(binary)
:: {binary, {:report_calibration, binary, :idle | :home | :end}}
defp parse_report_calibration(r) do
[axis_and_status | [q]] = String.split(r, " Q")
<<a::size(8), b::size(8)>> = axis_and_status
case <<b>> do
"0" -> {q, {:report_calibration, <<a>>, :idle}}
"1" -> {q, {:report_calibration, <<a>>, :home}}
"2" -> {q, {:report_calibration, <<a>>, :end}}
end
end
defp parse_report_axis_calibration(params) do
["P" <> parm, "V" <> val, "Q" <> tag] = String.split(params, " ")
if parm in ["141", "142", "143"] do
parm_name = :report_axis_calibration
result = parse_param(String.to_integer(parm))
case Float.parse(val) do
{float, _} ->
msg = {parm_name, result, float}
{tag, msg}
:error ->
msg = {parm_name, result, String.to_integer(val)}
{tag, msg}
end
else
{tag, :noop}
end
end
@spec parse_version(binary) :: {binary, {:report_software_version, binary}}
defp parse_version(version) do
[v | [code]] = String.split(version, " Q")
{code, {:report_software_version, v}}
end
@type reporter ::
:report_current_position
| :report_encoder_position_scaled
| :report_encoder_position_raw
@spec report_xyz(binary, reporter)
:: {binary, {reporter, float(), float(), float()}}
defp report_xyz(position, reporter) when is_bitstring(position),
do: position |> String.split(" ") |> do_parse_pos(reporter)
@valid_position_reporters [
:report_current_position,
:report_encoder_position_scaled
]
defp do_parse_pos(["X" <> x, "Y" <> y, "Z" <> z, "Q" <> tag], reporter)
when reporter in @valid_position_reporters
do
import String, only: [to_float: 1]
msg = {reporter, to_float(x), to_float(y), to_float(z)}
{tag, msg}
end
defp do_parse_pos(["X" <> x, "Y" <> y, "Z" <> z, "Q" <> tag], reporter) do
import String, only: [to_integer: 1]
msg = {reporter, to_integer(x), to_integer(y), to_integer(z)}
{tag, msg}
end
defp do_parse_pos(l, _) do
{:unhandled_gcode, Enum.join(l, " ")}
end
@doc false
@spec parse_end_stops(binary)
:: {binary(), {:report_end_stops, 0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1, 0 | 1}}
def parse_end_stops(
<<"XA", xa::size(8), 32,
"XB", xb::size(8), 32,
"YA", ya::size(8), 32,
"YB", yb::size(8), 32,
"ZA", za::size(8), 32,
"ZB", zb::size(8), 32,
"Q", tag::binary >>)
do
r = :report_end_stops
msg = {r, xa |> pes, xb |> pes, ya |> pes, yb |> pes, za |> pes, zb |> pes}
{tag, msg}
end
# lol
@spec pes(48 | 49) :: 0 | 1
defp pes(48), do: 0
defp pes(49), do: 1
def parse_pvq(params, :report_parameter_value)
when is_bitstring(params),
do: params |> String.split(" ") |> do_parse_params
def parse_pvq(params, human_readable_param_name)
when is_bitstring(params) and is_atom(human_readable_param_name),
do: params |> String.split(" ") |> do_parse_pvq(human_readable_param_name)
defp do_parse_pvq([p, v, q], human_readable_param_name) do
import String, only: [split: 2, to_integer: 1]
[_, rp] = split(p, "P")
[_, rv] = split(v, "V")
[_, rq] = split(q, "Q")
{rq, {human_readable_param_name, to_integer(rp), to_integer(rv)}}
end
defp do_parse_params([p, v, q]) do
import String, only: [split: 2, to_integer: 1]
[_, rp] = split(p, "P")
[_, rv] = split(v, "V")
[_, rq] = split(q, "Q")
{rq, {:report_parameter_value, parse_param(to_integer(rp)), to_integer(rv)}}
end
end

View File

@ -1,86 +0,0 @@
defmodule Farmbot.Firmware.Handler do
@moduledoc """
Any module that implements this behaviour should be a GenStage.
The implementng stage should communicate with the various Farmbot
hardware such as motors and encoders. The `Farmbot.Firmware` module
will subscribe_to: the implementing handler. Events should be
Gcodes as parsed by `Farmbot.Firmware.Gcode.Parser`.
"""
@typedoc "Pid of a firmware implementation."
@type handler :: pid
@doc "Start a firmware handler."
@callback start_link :: {:ok, handler}
@typedoc false
@type fw_ret_val :: :ok | {:error, term}
@typedoc false
@type vec3 :: Farmbot.Firmware.Vec3.t
@typedoc false
@type axis :: Farmbot.Firmware.Vec3.axis
@typedoc false
@type fw_param :: Farmbot.Firmware.Gcode.Param.t
@typedoc "Speed of a command."
@type speed :: number
@typedoc "Pin"
@type pin :: number
@typedoc "Mode of a pin."
@type pin_mode :: :digital | :analog
@doc "Move to a position."
@callback move_absolute(handler, vec3, speed, speed, speed) :: fw_ret_val
@doc "Calibrate an axis."
@callback calibrate(handler, axis) :: fw_ret_val
@doc "Find home on an axis."
@callback find_home(handler, axis) :: fw_ret_val
@doc "Manually set an axis's current position to zero."
@callback zero(handler, axis) :: fw_ret_val
@doc "Home an axis."
@callback home(handler, axis) :: fw_ret_val
@doc "Home every axis."
@callback home_all(handler) :: fw_ret_val
@doc "Update a paramater."
@callback update_param(handler, fw_param, number) :: fw_ret_val
@doc "Read a paramater."
@callback read_param(handler, fw_param) :: fw_ret_val
@doc "Read all params"
@callback read_all_params(handler) :: fw_ret_val
@doc "Lock the firmware."
@callback emergency_lock(handler) :: fw_ret_val
@doc "Unlock the firmware."
@callback emergency_unlock(handler) :: fw_ret_val
@doc "Read a pin."
@callback read_pin(handler, pin, pin_mode) :: fw_ret_val
@doc "Write a pin."
@callback write_pin(handler, pin, pin_mode, number) :: fw_ret_val
@doc "Set a pin mode (input/output)"
@callback set_pin_mode(handler, pin, :input | :input_pullup | :output) :: fw_ret_val
@doc "Request firmware version."
@callback request_software_version(handler) :: fw_ret_val
@doc "Set angle on a servo pin."
@callback set_servo_angle(handler, pin, number) :: fw_ret_val
end

View File

@ -1,24 +0,0 @@
defmodule Farmbot.Firmware.Supervisor do
@moduledoc false
use Supervisor
@doc "Reinitializes the Firmware stack. Warning has MANY SIDE EFFECTS."
def reinitialize do
Farmbot.Firmware.UartHandler.AutoDetector.start_link([])
Supervisor.terminate_child(Farmbot.Core, Farmbot.Firmware.Supervisor)
end
@doc false
def start_link(args) do
Supervisor.start_link(__MODULE__, args, [name: __MODULE__])
end
def init([]) do
children = [
{Farmbot.Firmware.EstopTimer, []},
{Farmbot.Firmware, []},
]
Supervisor.init(children, [strategy: :one_for_one])
end
end

View File

@ -1,39 +0,0 @@
defmodule Farmbot.Firmware.Utils do
@moduledoc """
Helpful utilities for working with Firmware data.
"""
@compile {:inline, [num_to_bool: 1]}
@doc "changes a number to a boolean. 1 => true, 0 => false"
def num_to_bool(num) when num == 1, do: true
def num_to_bool(num) when num == 0, do: false
@compile {:inline, [fmnt_float: 1]}
@doc "Format a float to a binary with two leading decimals."
def fmnt_float(num) when is_float(num),
do: :erlang.float_to_binary(num, [:compact, {:decimals, 2}])
def fmnt_float(num) when is_integer(num), do: fmnt_float(num / 1)
@compile {:inline, [extract_pin_mode: 1]}
@doc "Changes `:digital` => 0, and `:analog` => 1"
def extract_pin_mode(:digital), do: 0
def extract_pin_mode(:analog), do: 1
def extract_pin_mode(0), do: 0
def extract_pin_mode(1), do: 1
# https://github.com/arduino/Arduino/blob/2bfe164b9a5835e8cb6e194b928538a9093be333/hardware/arduino/avr/cores/arduino/Arduino.h#L43-L45
@compile {:inline, [extract_set_pin_mode: 1]}
@doc "Changes `set_pin_mode` arg to an integer for the Firmware."
def extract_set_pin_mode(:input), do: 0x0
def extract_set_pin_mode(:input_pullup), do: 0x2
def extract_set_pin_mode(:output), do: 0x1
@doc "replace the firmware handler at runtime."
def replace_firmware_handler(handler) do
old = Application.get_all_env(:farmbot_core)[:behaviour]
new = Keyword.put(old, :firmware_handler, handler)
Application.put_env(:farmbot_core, :behaviour, new)
end
end

View File

@ -1,30 +0,0 @@
defmodule Farmbot.Firmware.Vec3 do
@moduledoc "A three position vector."
alias Farmbot.Firmware.Vec3
defstruct [x: -1.0, y: -1.0, z: -1.0]
@typedoc "Axis label."
@type axis :: :x | :y | :z
@typedoc @moduledoc
@type t :: %__MODULE__{x: number, y: number, z: number}
def new(x, y, z) do
%Vec3{x: x, y: y, z: z}
end
end
defimpl Inspect, for: Farmbot.Firmware.Vec3 do
import Farmbot.Firmware.Utils, only: [fmnt_float: 1]
def inspect(vec3, _) do
"(#{fmnt_float(vec3.x)}, #{fmnt_float(vec3.y)}, #{fmnt_float(vec3.z)})"
end
end
defimpl String.Chars, for: Farmbot.Firmware.Vec3 do
import Farmbot.Firmware.Utils, only: [fmnt_float: 1]
def to_string(vec3) do
"(#{fmnt_float(vec3.x)}, #{fmnt_float(vec3.y)}, #{fmnt_float(vec3.z)})"
end
end

View File

@ -45,6 +45,8 @@ defmodule Farmbot.Log do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
schema "logs" do
field(:level, LogLevelType)
field(:verbosity, :integer)
@ -62,7 +64,7 @@ defmodule Farmbot.Log do
end
@required_fields [:level, :verbosity, :message]
@optional_fields [:meta, :function, :file, :line, :module]
@optional_fields [:meta, :function, :file, :line, :module, :id, :inserted_at, :updated_at]
def changeset(log, params \\ %{}) do
log
@ -88,13 +90,13 @@ defmodule Farmbot.Log do
end
end
defp color(:debug), do: IO.ANSI.light_blue()
defp color(:info), do: IO.ANSI.cyan()
defp color(:busy), do: IO.ANSI.blue()
defp color(:debug), do: IO.ANSI.light_blue()
defp color(:info), do: IO.ANSI.cyan()
defp color(:busy), do: IO.ANSI.blue()
defp color(:success), do: IO.ANSI.green()
defp color(:warn), do: IO.ANSI.yellow()
defp color(:error), do: IO.ANSI.red()
defp color(:normal), do: IO.ANSI.normal()
defp color(_), do: IO.ANSI.normal()
defp color(:warn), do: IO.ANSI.yellow()
defp color(:error), do: IO.ANSI.red()
defp color(:normal), do: IO.ANSI.normal()
defp color(_), do: IO.ANSI.normal()
end
end

View File

@ -4,6 +4,7 @@ defmodule Farmbot.Logger do
"""
alias Farmbot.Logger.Repo
import Ecto.Query
@doc "Send a debug message to log endpoints"
defmacro debug(verbosity, message, meta \\ []) do
@ -69,23 +70,21 @@ defmodule Farmbot.Logger do
@doc "Gets all available logs and deletes them."
def handle_all_logs do
Repo.all(Farmbot.Log)
|> Enum.map(&Repo.delete!(&1))
Repo.all(from(l in Farmbot.Log, order_by: l.inserted_at))
|> Enum.map(&Repo.delete!/1)
end
@doc false
def dispatch_log(%Macro.Env{} = env, level, verbosity, message, meta)
when level in [:info, :debug, :busy, :warn, :success, :error, :fun]
and is_number(verbosity)
and is_binary(message)
and is_list(meta)
do
fun = case env.function do
{fun, ar} -> "#{fun}/#{ar}"
nil -> "no_function"
end
when level in [:info, :debug, :busy, :warn, :success, :error, :fun] and is_number(verbosity) and
is_binary(message) and is_list(meta) do
fun =
case env.function do
{fun, ar} -> "#{fun}/#{ar}"
nil -> "no_function"
end
struct(Farmbot.Log, [
struct(Farmbot.Log,
level: level,
verbosity: verbosity,
message: message,
@ -93,7 +92,8 @@ defmodule Farmbot.Logger do
function: fun,
file: env.file,
line: env.line,
module: env.module])
module: env.module
)
|> dispatch_log()
end
@ -102,9 +102,6 @@ defmodule Farmbot.Logger do
log
|> insert_log!()
|> elixir_log()
|> fn(log) ->
Farmbot.Registry.dispatch(__MODULE__, {:log_ready, log.id})
end.()
end
defp elixir_log(%Farmbot.Log{} = log) do
@ -114,9 +111,10 @@ defmodule Farmbot.Logger do
function: log.function,
file: log.file,
line: log.line,
module: log.module,
module: log.module
# time: time
]
level = log.level
logger_level = if level in [:info, :debug, :warn, :error], do: level, else: :info
Elixir.Logger.bare_log(logger_level, log, logger_meta)
@ -129,7 +127,7 @@ defmodule Farmbot.Logger do
def should_log?(nil, _), do: false
def should_log?(module, verbosity) when verbosity <= 3 do
List.first(Module.split(module)) == "Farmbot"
List.first(Module.split(module)) == "Farmbot"
end
def should_log?(_, _), do: false

View File

@ -10,6 +10,7 @@ defmodule Farmbot.Logger.Supervisor do
children = [
supervisor(Farmbot.Logger.Repo, [])
]
opts = [strategy: :one_for_all]
supervise(children, opts)
end

View File

@ -1,45 +0,0 @@
defmodule Farmbot.Registry do
@moduledoc "Farmbot System Global Registry"
@reg FarmbotRegistry
use GenServer
@doc false
def start_link(args) do
GenServer.start_link(__MODULE__, args, [name: __MODULE__])
end
@doc "Dispatch a global event from a namespace."
def dispatch(namespace, event) do
GenServer.call(__MODULE__, {:dispatch, namespace, event})
end
def subscribe(pid \\ self()) do
Elixir.Registry.register(@reg, __MODULE__, pid)
end
def drop_pattern(pattern, me, acc \\ []) do
receive do
{__MODULE__, {^pattern, _}} -> drop_pattern(pattern, me, acc)
other -> drop_pattern(pattern, me, [other | acc])
after 100 ->
for msg <- Enum.reverse(acc) do
send(me, msg)
end
end
end
def init([]) do
# partitions = System.schedulers_online
partitions = 1
opts = [keys: :duplicate, partitions: partitions, name: @reg]
{:ok, reg} = Elixir.Registry.start_link(opts)
{:ok, %{reg: reg}}
end
def handle_call({:dispatch, ns, event}, _from, state) do
Elixir.Registry.dispatch(@reg, __MODULE__, fn(entries) ->
for {pid, _} <- entries, do: send(pid, {__MODULE__, {ns, event}})
end)
{:reply, :ok, state}
end
end

View File

@ -65,9 +65,8 @@ defmodule FarmbotCore.MixProject do
defp deps do
[
{:farmbot_celery_script, path: "../farmbot_celery_script", env: Mix.env()},
{:farmbot_firmware, path: "../farmbot_firmware", env: Mix.env()},
{:elixir_make, "~> 0.4", runtime: false},
{:nerves_uart, "~> 1.2"},
{:gen_stage, "~> 0.14"},
{:sqlite_ecto2, "~> 2.3"},
{:timex, "~> 3.4"},
{:plug_cowboy, "~> 2.0"},

View File

@ -10,8 +10,10 @@ defmodule Elixir.Farmbot.Asset.Repo.Migrations.CreateFbosConfigsTable do
add(:beta_opt_in, :boolean)
add(:disable_factory_reset, :boolean)
add(:firmware_hardware, :string)
add(:firmware_path, :string)
add(:firmware_input_log, :boolean)
add(:firmware_output_log, :boolean)
add(:firmware_debug_log, :boolean)
add(:network_not_found_timer, :integer)
add(:os_auto_update, :boolean)
add(:sequence_body_log, :boolean)

View File

@ -2,10 +2,10 @@ defmodule Farmbot.Config.Repo.Migrations.AddFirmwareIoLog do
use Ecto.Migration
import Farmbot.Config.MigrationHelpers
@io_logs Application.get_env(:farmbot_core, :firmware_io_logs, false)
@default_firmware_io_logs Application.get_env(:farmbot_core, :default_firmware_io_logs, false)
def change do
create_settings_config("firmware_input_log", :bool, @io_logs)
create_settings_config("firmware_output_log", :bool, @io_logs)
create_settings_config("firmware_input_log", :bool, @default_firmware_io_logs)
create_settings_config("firmware_output_log", :bool, @default_firmware_io_logs)
end
end

View File

@ -2,7 +2,8 @@ defmodule Farmbot.Logger.Repo.Migrations.AddLogBuffer do
use Ecto.Migration
def change do
create table("logs") do
create table("logs", primary_key: false) do
add(:id, :binary_id, primary_key: true)
add(:message, :text)
add(:level, :string)
add(:verbosity, :integer)

View File

@ -0,0 +1,45 @@
defmodule Farmbot.FbosConfigWorkerTest do
use ExUnit.Case
alias Farmbot.Asset.FbosConfig
test "adds configs to bot state and config_storage" do
conf =
FbosConfig.changeset(%FbosConfig{}, %{
arduino_debug_messages: true,
auto_sync: false,
beta_opt_in: true,
disable_factory_reset: false,
firmware_hardware: "farmduino_k14",
firmware_input_log: false,
firmware_output_log: false,
id: 145,
network_not_found_timer: nil,
os_auto_update: false,
sequence_body_log: true,
sequence_complete_log: true,
sequence_init_log: true
})
|> Farmbot.Asset.Repo.insert!()
:ok = Farmbot.AssetMonitor.force_checkup()
# Wait for the timeout to be dispatched
Process.sleep(100)
state_conf = Farmbot.BotState.fetch().configuration
assert state_conf.arduino_debug_messages == conf.arduino_debug_messages
assert state_conf.auto_sync == conf.auto_sync
assert state_conf.beta_opt_in == conf.beta_opt_in
assert state_conf.disable_factory_reset == conf.disable_factory_reset
assert state_conf.firmware_hardware == conf.firmware_hardware
assert state_conf.firmware_input_log == conf.firmware_input_log
assert state_conf.firmware_output_log == conf.firmware_output_log
assert state_conf.network_not_found_timer == conf.network_not_found_timer
assert state_conf.os_auto_update == conf.os_auto_update
assert state_conf.sequence_body_log == conf.sequence_body_log
assert state_conf.sequence_complete_log == conf.sequence_complete_log
assert state_conf.sequence_init_log == conf.sequence_init_log
# TODO(Connor) assert config_storage
end
end

View File

@ -0,0 +1,94 @@
defmodule Farmbot.BotStateNGTest do
use ExUnit.Case, async: true
alias Farmbot.BotStateNG
describe "pins" do
test "adds pins to the state" do
orig = BotStateNG.new()
assert Enum.empty?(orig.pins)
one_pin =
BotStateNG.add_or_update_pin(orig, 10, 1, 2)
|> Ecto.Changeset.apply_changes()
assert one_pin.pins[10] == %{mode: 1, value: 2}
two_pins =
BotStateNG.add_or_update_pin(one_pin, 20, 1, 20)
|> Ecto.Changeset.apply_changes()
assert two_pins.pins[10] == %{mode: 1, value: 2}
assert two_pins.pins[20] == %{mode: 1, value: 20}
end
test "updates an existing pin" do
orig = BotStateNG.new()
assert Enum.empty?(orig.pins)
one_pin =
BotStateNG.add_or_update_pin(orig, 10, 1, 2)
|> Ecto.Changeset.apply_changes()
assert one_pin.pins[10] == %{mode: 1, value: 2}
one_pin_updated =
BotStateNG.add_or_update_pin(one_pin, 10, 1, 50)
|> Ecto.Changeset.apply_changes()
assert one_pin_updated.pins[10] == %{mode: 1, value: 50}
end
end
describe "informational_settings" do
test "reports soc_temp" do
orig = BotStateNG.new()
mut =
BotStateNG.changeset(orig, %{informational_settings: %{soc_temp: 100}})
|> Ecto.Changeset.apply_changes()
assert mut.informational_settings.soc_temp == 100
end
test "reports disk_usage" do
orig = BotStateNG.new()
mut =
BotStateNG.changeset(orig, %{informational_settings: %{disk_usage: 100}})
|> Ecto.Changeset.apply_changes()
assert mut.informational_settings.disk_usage == 100
end
test "reports memory_usage" do
orig = BotStateNG.new()
mut =
BotStateNG.changeset(orig, %{informational_settings: %{memory_usage: 512}})
|> Ecto.Changeset.apply_changes()
assert mut.informational_settings.memory_usage == 512
end
test "reports uptime" do
orig = BotStateNG.new()
mut =
BotStateNG.changeset(orig, %{informational_settings: %{uptime: 5000}})
|> Ecto.Changeset.apply_changes()
assert mut.informational_settings.uptime == 5000
end
test "reports wifi_level" do
orig = BotStateNG.new()
mut =
BotStateNG.changeset(orig, %{informational_settings: %{wifi_level: 52}})
|> Ecto.Changeset.apply_changes()
assert mut.informational_settings.wifi_level == 52
end
end
end

View File

@ -0,0 +1,42 @@
defmodule Farmbot.BotStateTest do
use ExUnit.Case
alias Farmbot.BotState
describe "bot state pub/sub" do
test "subscribes to bot state updates" do
{:ok, bot_state_pid} = BotState.start_link([], [])
_initial_state = BotState.subscribe(bot_state_pid)
:ok = BotState.set_user_env(bot_state_pid, "some_key", "some_val")
assert_receive {BotState, %Ecto.Changeset{valid?: true}}
end
test "invalid data doesn't get dispatched" do
{:ok, bot_state_pid} = BotState.start_link([], [])
_initial_state = BotState.subscribe(bot_state_pid)
result = BotState.report_disk_usage(bot_state_pid, "this is invalid")
assert match?({:error, %Ecto.Changeset{valid?: false}}, result)
refute_receive {BotState, %Ecto.Changeset{valid?: true}}
end
test "subscribing links current process" do
# Trap exits so we can assure we can see bot the
# BotState processess and the subscriber process crash.
Process.flag(:trap_exit, true)
# two links, BotState and Subscriber
{:ok, bot_state_pid} = BotState.start_link([], [])
fun = fn ->
_initial_state = BotState.subscribe(bot_state_pid)
exit(:crash)
end
# Spawn the subscriber function
fun_pid = spawn_link(fun)
# Make sure both BotState and Subscriber crashes
assert_receive {:EXIT, ^fun_pid, :crash}
assert_receive {:EXIT, ^bot_state_pid, :crash}
end
end
end

View File

@ -1,12 +0,0 @@
defmodule Farmbot.BotStateTest do
use ExUnit.Case, async: false
alias Farmbot.{BotState, Config}
test "writing config values goes into state" do
Config.update_config_value(:bool, "settings", "log_amqp_connected", true)
assert BotState.fetch().configuration.log_amqp_connected
Config.update_config_value(:bool, "settings", "log_amqp_connected", false)
refute BotState.fetch().configuration.log_amqp_connected
end
end

View File

@ -0,0 +1,34 @@
defmodule Farmbot.Core.FirmwareEstopTimerTest do
use ExUnit.Case
alias Farmbot.Core.FirmwareEstopTimer
test "calls a function in X MS" do
test_pid = self()
timeout_ms = :rand.uniform(20)
timeout_function = fn ->
send(test_pid, :estop_timer_message)
end
args = [timeout_function: timeout_function, timeout_ms: timeout_ms]
{:ok, pid} = FirmwareEstopTimer.start_link(args, [])
_timer = FirmwareEstopTimer.start_timer(pid)
assert_receive :estop_timer_message, timeout_ms + 5
end
test "doesn't call function if canceled" do
timeout_ms = :rand.uniform(20)
test_pid = self()
timeout_function = fn ->
send(test_pid, :estop_timer_message)
flunk("This function should never be called")
end
args = [timeout_function: timeout_function, timeout_ms: timeout_ms]
{:ok, pid} = FirmwareEstopTimer.start_link(args, [])
timer = FirmwareEstopTimer.start_timer(pid)
^timer = FirmwareEstopTimer.cancel_timer(pid)
refute_receive :estop_timer_message
end
end

View File

@ -0,0 +1,19 @@
defmodule Farmbot.LoggerTest do
use ExUnit.Case
require Farmbot.Logger
test "allows handling a log more than once by re-inserting it." do
log = Farmbot.Logger.debug(1, "Test log ABC")
# Handling a log should delete it from the store.
assert Enum.find(Farmbot.Logger.handle_all_logs(), &Kernel.==(Map.fetch!(&1, :id), log.id))
# Thus, handling all logs again should mean the log
# isn't there any more
refute Enum.find(Farmbot.Logger.handle_all_logs(), &Kernel.==(Map.fetch!(&1, :id), log.id))
# insert the log again
assert Farmbot.Logger.insert_log!(log)
# Make sure the log is available for handling again.
assert Enum.find(Farmbot.Logger.handle_all_logs(), &Kernel.==(Map.fetch!(&1, :id), log.id))
end
end

View File

@ -1,26 +1,24 @@
use Mix.Config
config :farmbot_core, Farmbot.AssetWorker.Farmbot.Asset.FarmEvent,
checkup_time_ms: 10_000
config :farmbot_core, Farmbot.AssetWorker.Farmbot.Asset.FarmEvent, checkup_time_ms: 10_000
config :farmbot_core, Farmbot.AssetMonitor,
checkup_time_ms: 30_000
config :farmbot_core, Elixir.Farmbot.AssetWorker.Farmbot.Asset.PinBinding,
gpio_handler: Farmbot.PinBindingWorker.StubGPIOHandler,
error_retry_time_ms: 30_000
config :farmbot_core, Farmbot.AssetWorker.Farmbot.Asset.FarmwareInstallation,
error_retry_time_ms: 30_000,
install_dir: "/tmp/farmware"
config :farmbot_core, Farmbot.AssetMonitor, checkup_time_ms: 30_000
config :farmbot_core, :behaviour,
firmware_handler: Farmbot.Firmware.StubHandler,
leds_handler: Farmbot.Leds.StubHandler,
pin_binding_handler: Farmbot.PinBinding.StubHandler,
celery_script_io_layer: Farmbot.Core.CeleryScript.StubIOLayer,
json_parser: Farmbot.JSON.JasonParser
config :farmbot_core,
ecto_repos: [Farmbot.Config.Repo, Farmbot.Logger.Repo, Farmbot.Asset.Repo],
expected_fw_versions: ["6.4.0.F", "6.4.0.R", "6.4.0.G"],
default_firmware_io_logs: false,
default_server: "https://my.farm.bot",
default_currently_on_beta:
String.contains?(to_string(:os.cmd('git rev-parse --abbrev-ref HEAD')), "beta"),
firmware_io_logs: false,
farm_event_debug_log: false
String.contains?(to_string(:os.cmd('git rev-parse --abbrev-ref HEAD')), "beta")

View File

@ -3,6 +3,9 @@ defmodule Farmbot.AMQP.BotStateTransport do
use AMQP
require Farmbot.Logger
# Pushes a state tree every 5 seconds for good luck.
@default_force_time_ms 5_000
@default_error_retry_ms 100
@exchange "amq.topic"
defstruct [:conn, :chan, :bot, :state_cache]
@ -19,37 +22,38 @@ defmodule Farmbot.AMQP.BotStateTransport do
def init([conn, jwt]) do
Process.flag(:sensitive, true)
Farmbot.Registry.subscribe()
initial_bot_state = Farmbot.BotState.subscribe()
{:ok, chan} = AMQP.Channel.open(conn)
:ok = Basic.qos(chan, global: true)
{:ok, struct(State, conn: conn, chan: chan, bot: jwt.bot)}
{:ok, struct(State, conn: conn, chan: chan, bot: jwt.bot, state_cache: initial_bot_state), 0}
end
def handle_cast(:force, %{state_cache: bot_state} = state) do
push_bot_state(state.chan, state.bot, bot_state)
{:noreply, state}
def handle_cast(:force, state) do
{:noreply, state, 0}
end
def handle_info(
{Farmbot.Registry, {Farmbot.BotState, bot_state}},
%{state_cache: bot_state} = state
) do
# IO.puts "no state change"
{:noreply, state}
def handle_info(:timeout, %{state_cache: bot_state} = state) do
case push_bot_state(state.chan, state.bot, bot_state) do
:ok ->
{:noreply, state, @default_force_time_ms}
error ->
Farmbot.Logger.error(1, "Failed to dispatch BotState: #{inspect(error)}")
{:noreply, state, @default_error_retry_ms}
end
end
def handle_info({Farmbot.Registry, {Farmbot.BotState, bot_state}}, state) do
# IO.puts "pushing state"
state.state_cache
cache = push_bot_state(state.chan, state.bot, bot_state)
{:noreply, %{state | state_cache: cache}}
def handle_info({Farmbot.BotState, change}, state) do
new_state_cache = Ecto.Changeset.apply_changes(change)
{:noreply, %{state | state_cache: new_state_cache}, 0}
end
def handle_info({Farmbot.Registry, _}, state), do: {:noreply, state}
defp push_bot_state(chan, bot, %Farmbot.BotStateNG{} = bot_state) do
json =
bot_state
|> Farmbot.BotStateNG.view()
|> Farmbot.JSON.encode!()
defp push_bot_state(chan, bot, state) do
json = Farmbot.JSON.encode!(state)
:ok = AMQP.Basic.publish(chan, @exchange, "bot.#{bot}.status", json)
state
AMQP.Basic.publish(chan, @exchange, "bot.#{bot}.status", json)
end
end

View File

@ -2,9 +2,11 @@ defmodule Farmbot.AMQP.LogTransport do
use GenServer
use AMQP
require Farmbot.Logger
require Logger
import Farmbot.Config, only: [update_config_value: 4]
@exchange "amq.topic"
@checkup_ms 100
defstruct [:conn, :chan, :bot, :state_cache]
alias __MODULE__, as: State
@ -16,16 +18,11 @@ defmodule Farmbot.AMQP.LogTransport do
def init([conn, jwt]) do
Process.flag(:sensitive, true)
Farmbot.Registry.subscribe()
initial_bot_state = Farmbot.BotState.subscribe()
{:ok, chan} = AMQP.Channel.open(conn)
:ok = Basic.qos(chan, global: true)
state = struct(State, conn: conn, chan: chan, bot: jwt.bot)
for l <- Farmbot.Logger.handle_all_logs() do
do_handle_log(l, state)
end
{:ok, state}
state = struct(State, conn: conn, chan: chan, bot: jwt.bot, state_cache: initial_bot_state)
{:ok, state, 0}
end
def terminate(reason, state) do
@ -41,20 +38,30 @@ defmodule Farmbot.AMQP.LogTransport do
if state.chan, do: AMQP.Channel.close(state.chan)
end
def handle_info({Farmbot.Registry, {Farmbot.Logger, {:log_ready, id}}}, state) do
if log = Farmbot.Logger.handle_log(id) do
do_handle_log(log, state)
def handle_info({Farmbot.BotState, change}, state) do
new_state_cache = Ecto.Changeset.apply_changes(change)
{:noreply, %{state | state_cache: new_state_cache}, @checkup_ms}
end
def handle_info(:timeout, state) do
{:noreply, state, {:continue, Farmbot.Logger.handle_all_logs()}}
end
def handle_continue([log | rest], state) do
case do_handle_log(log, state) do
:ok ->
{:noreply, state, {:continue, rest}}
error ->
Logger.error("Logger amqp client failed to upload log: #{inspect(error)}")
# Reschedule log to be uploaded again
Farmbot.Logger.insert_log!(log)
{:noreply, state, @checkup_ms}
end
{:noreply, state}
end
def handle_info({Farmbot.Registry, {Farmbot.BotState, bot_state}}, state) do
{:noreply, %{state | state_cache: bot_state}}
end
def handle_info({Farmbot.Registry, _}, state) do
{:noreply, state}
def handle_continue([], state) do
{:noreply, state, @checkup_ms}
end
defp do_handle_log(log, state) do
@ -71,13 +78,15 @@ defmodule Farmbot.AMQP.LogTransport do
major_version: log.version.major,
minor_version: log.version.minor,
patch_version: log.version.patch,
# QUESTION(Connor) - Why does this need `.to_unix()`?
# ANSWER(Connor) - because the FE needed it.
created_at: DateTime.from_naive!(log.inserted_at, "Etc/UTC") |> DateTime.to_unix(),
channels: log.meta[:channels] || [],
message: log.message
}
log = add_position_to_log(log_without_pos, location_data)
push_bot_log(state.chan, state.bot, log)
json_log = add_position_to_log(log_without_pos, location_data)
push_bot_log(state.chan, state.bot, json_log)
end
end
@ -86,7 +95,7 @@ defmodule Farmbot.AMQP.LogTransport do
:ok = AMQP.Basic.publish(chan, @exchange, "bot.#{bot}.logs", json)
end
defp add_position_to_log(%{} = log, %{position: %{} = pos}) do
Map.merge(log, pos)
defp add_position_to_log(%{} = log, %{position: %{x: x, y: y, z: z}}) do
Map.merge(log, %{x: x, y: y, z: z})
end
end

View File

@ -60,13 +60,20 @@ defmodule Farmbot.API do
content_length = :filelib.file_size(image_filename)
{:ok, pid} = Agent.start_link(fn -> 0 end)
prog = %Percent{
status: :working,
percent: 0,
time: DateTime.utc_now(),
type: :image
}
stream =
image_filename
|> File.stream!([], @file_chunk)
|> Stream.each(fn chunk ->
Agent.update(pid, fn sent ->
size = sent + byte_size(chunk)
prog = put_progress(size, content_length)
prog = put_progress(prog, size, content_length)
BotState.set_job_progress(image_filename, prog)
size
end)
@ -95,17 +102,17 @@ defmodule Farmbot.API do
client <- API.client(),
body <- %{attachment_url: attachment_url, meta: meta},
{:ok, %{status: s}} = r when s > 199 and s < 300 <- API.post(client, "/api/images", body) do
Farmbot.BotState.set_job_progress(image_filename, %Percent{percent: 100, status: :complete})
Farmbot.BotState.set_job_progress(image_filename, %{prog | status: :complete, percent: 100})
r
else
er ->
Farmbot.Logger.error(1, "Failed to upload image")
Farmbot.BotState.set_job_progress(image_filename, %Percent{percent: -1, status: :error})
Farmbot.BotState.set_job_progress(image_filename, %{prog | percent: -1, status: :error})
er
end
end
def put_progress(size, max) do
def put_progress(prog, size, max) do
fraction = size / max
completed = trunc(fraction * @progress_steps)
percent = trunc(fraction * 100)
@ -121,8 +128,9 @@ defmodule Farmbot.API do
status = if percent == 100, do: :complete, else: :working
%Percent{
status: status,
percent: percent
prog
| status: status,
percent: percent
}
end

View File

@ -1 +0,0 @@

View File

@ -1,14 +1,14 @@
# DELETEME
require Protocol
# Bot State
Protocol.derive(Jason.Encoder, Farmbot.BotState)
Protocol.derive(Jason.Encoder, Farmbot.BotState.Configuration)
Protocol.derive(Jason.Encoder, Farmbot.BotState.InformationalSettings)
Protocol.derive(Jason.Encoder, Farmbot.BotState.LocationData)
Protocol.derive(Jason.Encoder, Farmbot.BotState.McuParams)
Protocol.derive(Jason.Encoder, Farmbot.BotState.Pin)
Protocol.derive(Jason.Encoder, Farmbot.BotState.JobProgress.Bytes)
Protocol.derive(Jason.Encoder, Farmbot.BotState.JobProgress.Percent)
# Protocol.derive(Jason.Encoder, Farmbot.BotState)
# Protocol.derive(Jason.Encoder, Farmbot.BotState.Configuration)
# Protocol.derive(Jason.Encoder, Farmbot.BotState.InformationalSettings)
# Protocol.derive(Jason.Encoder, Farmbot.BotState.LocationData)
# Protocol.derive(Jason.Encoder, Farmbot.BotState.McuParams)
# Protocol.derive(Jason.Encoder, Farmbot.BotState.Pin)
# Protocol.derive(Jason.Encoder, Farmbot.BotState.JobProgress.Bytes)
# Protocol.derive(Jason.Encoder, Farmbot.BotState.JobProgress.Percent)
Protocol.derive(Jason.Encoder, Farmbot.JWT)

View File

@ -1,8 +0,0 @@
defmodule FarmbotExtTest do
use ExUnit.Case
doctest FarmbotExt
test "greets the world" do
assert FarmbotExt.hello() == :world
end
end

View File

@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

24
farmbot_firmware/.gitignore vendored 100644
View File

@ -0,0 +1,24 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
farmbot_firmware-*.tar

View File

@ -0,0 +1,21 @@
# FarmbotFirmware
**TODO: Add description**
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `farmbot_firmware` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:farmbot_firmware, "~> 0.1.0"}
]
end
```
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at [https://hexdocs.pm/farmbot_firmware](https://hexdocs.pm/farmbot_firmware).

View File

@ -0,0 +1 @@
use Mix.Config

View File

@ -0,0 +1,479 @@
defmodule Farmbot.Firmware do
@moduledoc """
Firmware wrapper for interacting with Farmbot-Arduino-Firmware.
This GenServer is expected to be a pretty simple state machine
with no side effects to anything in the rest of the Farmbot application.
Side effects should be implemented using a callback/pubsub system. This
allows for indpendent testing.
Functionality that is needed to boot the firmware:
* paramaters - Keyword list of {param_atom, float}
Side affects that should be handled
* position reports
* end stop reports
* calibration reports
* busy reports
# State machine
The firmware starts in a `:boot` state. It then loads all paramaters
writes all paramaters, and goes to idle if all params were loaded successfully.
State machine flows go as follows:
## Boot
:boot
|> :no_config
|> :configuration
|> :idle
## Idle
:idle
|> :begin
|> :busy
|> :error | :invalid | :success
# Constraints and Exceptions
Commands will be queued as they received with some exceptions:
* if a command is currently executing (state is not `:idle`),
proceding commands will be queued in the order they are received.
* the `:emergency_lock` and `:emergency_unlock` commands go to the front
of the command queue and are started immediately.
* if a `report_emergency_lock` message is received at any point during a
commands execution, that command is considered an error.
(this does not apply to `:boot` state, since `:paramater_write`
is accepted while the firmware is locked.)
* all reports outside of control flow reports (:begin, :error, :invalid,
:success) will be discarded while in `:boot` state. This means while
boot, position updates, end stop updates etc are ignored.
# Transports
GCODES should be exchanged in the following format:
{tag, {command, args}}
* `tag` - binary integer. This is translated to the `Q` paramater.
* `command` - either a `RXX`, `FXX`, or `GXX` code.
* `args` - a list of arguments to be processed.
For example a report might look like:
{"123", {:report_some_information, [h: 10.00, u: 90.10]}}
and a command might look like:
{"555", {:fire_laser, [w: 100.00]}}
Numbers should be floats when possible. An Exeption to this is `:report_end_stops`
where there is only two values: `1` or `0`.
See the `GCODE` module for more information on available implemented GCODES.
a `Transport` should be a process that implements standard `GenServer`
behaviour.
Upon `init/1` the args passed in should be a Keyword list required to configure
the transport such as a serial device, etc. `args` will also contain a
`:handle_gcode` function that should be called everytime a GCODE is received.
Keyword.fetch!(args, :handle_gcode).({"999", {:report_software_version, ["Just a test!"]}})
a transport should also implement a `handle_call` clause like:
def handle_call({"166", {:paramater_write, [some_param: 100.00]}}, _from, state)
and reply with `:ok | {:error, term()}`
"""
use GenServer
require Logger
alias Farmbot.Firmware, as: State
alias Farmbot.{Firmware.GCODE, Firmware.Command, Firmware.Request}
@error_timeout_ms 2_000
@type status :: :boot | :no_config | :configuration | :idle | :emergency_lock
defstruct [
:transport,
:transport_pid,
:side_effects,
:status,
:tag,
:configuration_queue,
:command_queue,
:caller_pid,
:current
]
@type state :: %State{
transport: module(),
transport_pid: pid(),
side_effects: nil | module(),
status: status(),
tag: GCODE.tag(),
configuration_queue: [{GCODE.kind(), GCODE.args()}],
command_queue: [{pid(), GCODE.t()}],
caller_pid: nil | pid,
current: nil | GCODE.t()
}
@doc """
Command the firmware to do something. Takes a `{tag, {command, args}}`
GCODE. This command will be queued if there is already a command
executing. (this does not apply to `:emergency_lock` and `:emergency_unlock`)
## Response/Control Flow
When executed, `command` will block until one of the following respones
are received:
* `{:report_success, []}` -> `:ok`
* `{:report_invalid, []}` -> `{:error, :invalid_command}`
* `{:report_error, []}` -> `{:error, :firmware_error}`
* `{:report_emergency_lock, []}` -> {:error, :emergency_lock}`
If the firmware is in any of the following states:
* `:boot`
* `:no_config`
* `:configuration`
`command` will fail with `{:error, state}`
"""
defdelegate command(server \\ __MODULE__, code), to: Command
@doc """
Request data from the firmware.
Valid requests are of kind:
:paramater_read
:status_read
:pin_read
:end_stops_read
:position_read
:software_version_read
Will return `{:ok, {tag, {:report_*, args}}}` on success
or `{:error, term()}` on error.
"""
defdelegate request(server \\ __MODULE__, code), to: Request
@doc """
Starting the Firmware server requires at least:
* `:transport` - a module implementing the Transport GenServer behaviour.
See the `Transports` section of moduledoc.
Every other arg passed in will be passed directly to the `:transport` module's
`init/1` function.
"""
def start_link(args, opts \\ [name: __MODULE__]) do
GenServer.start_link(__MODULE__, args, opts)
end
def init(args) do
transport = Keyword.fetch!(args, :transport)
side_effects = Keyword.get(args, :side_effects)
fw = self()
fun = fn {_, _} = code -> GenServer.cast(fw, code) end
args = Keyword.put(args, :handle_gcode, fun)
with {:ok, pid} <- GenServer.start_link(transport, args) do
Process.link(pid)
Logger.debug("Starting Firmware: #{inspect(args)}")
state = %State{
transport_pid: pid,
transport: transport,
side_effects: side_effects,
status: :boot,
command_queue: [],
configuration_queue: []
}
{:ok, state}
end
end
# @spec handle_info(:timeout, state) :: {:noreply, state}
def handle_info(:timeout, %{configuration_queue: [code | rest]} = state) do
Logger.debug("Starting next configuration code: #{inspect(code)}")
case GenServer.call(state.transport_pid, {state.tag, code}) do
:ok ->
new_state = %{state | current: code, configuration_queue: rest}
side_effects(new_state, :handle_output_gcode, [{state.tag, code}])
{:noreply, new_state}
{:error, _} ->
{:noreply, state, @error_timeout_ms}
end
end
def handle_info(:timeout, %{command_queue: [{pid, {tag, code}} | rest]} = state) do
case GenServer.call(state.transport_pid, {tag, code}) do
:ok ->
new_state = %{state | tag: tag, current: code, command_queue: rest, caller_pid: pid}
side_effects(new_state, :handle_output_gcode, [{state.tag, code}])
{:noreply, new_state}
{:error, _} ->
{:noreply, state, @error_timeout_ms}
end
end
def handle_info(:timeout, %{configuration_queue: []} = state) do
{:noreply, state}
end
def handle_call({_tag, _code} = gcode, from, state) do
handle_command(gcode, from, state)
end
@doc false
@spec handle_command(GCODE.t(), GenServer.from(), state()) :: {:reply, term(), state()}
def handle_command(_, _, %{status: s} = state) when s in [:boot, :no_config, :configuration] do
{:reply, {:error, s}, state}
end
def handle_command({tag, {:command_emergency_lock, []}} = code, {pid, _ref}, state) do
{:reply, {:ok, tag}, %{state | command_queue: [{pid, code} | state.command_queue]}, 0}
end
def handle_command({tag, {:command_emergency_unlock, []}} = code, {pid, _ref}, state) do
{:reply, {:ok, tag}, %{state | command_queue: [{pid, code} | state.command_queue]}, 0}
end
def handle_command({tag, {_, _}} = code, {pid, _ref}, state) do
new_state = %{state | command_queue: state.command_queue ++ [{pid, code}]}
case new_state.status do
:idle ->
{:reply, {:ok, tag}, new_state, 0}
# Don't do any flow control if state is emergency_lock.
# This allows a transport to decide
# if a command should be blocked or not.
:emergency_lock ->
{:reply, {:ok, tag}, new_state, 0}
_ ->
{:reply, {:ok, tag}, new_state}
end
end
# Extracts tag
def handle_cast({tag, {_, _} = code}, state) do
side_effects(state, :handle_input_gcode, [{tag, code}])
handle_report(code, %{state | tag: tag})
end
@doc false
@spec handle_report({GCODE.report_kind(), GCODE.args()}, state) ::
{:noreply, state(), 0} | {:noreply, state()}
def handle_report({:report_emergency_lock, []} = code, state) do
Logger.info("Emergency lock")
if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
{:noreply, goto(%{state | current: nil, caller_pid: nil}, :emergency_lock), 0}
end
# "ARDUINO STARTUP COMPLETE" => goto(:boot, :no_config)
def handle_report({:report_debug_message, ["ARDUINO STARTUP COMPLETE"]}, state) do
Logger.info("ARDUINO STARTUP COMPLETE")
{:noreply, goto(state, :no_config)}
end
def handle_report({:report_debug_message, msg}, state) do
side_effects(state, :handle_debug_message, [msg])
{:noreply, state}
end
def handle_report(report, %{status: :boot} = state) do
Logger.debug(["still in state: :boot ", inspect(report)])
{:noreply, state}
end
# report_idle => goto(_, :idle)
def handle_report({:report_idle, []}, %{status: _} = state) do
{:noreply, goto(%{state | caller_pid: nil, current: nil}, :idle), 0}
end
def handle_report({:report_begin, []} = code, state) do
if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
{:noreply, state}
end
def handle_report({:report_success, []} = code, state) do
if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
new_state = %{state | current: nil, caller_pid: nil}
if new_state.status == :emergency_lock do
{:noreply, goto(new_state, :idle), 0}
else
{:noreply, new_state, 0}
end
end
def handle_report({:report_busy, []} = code, state) do
if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
{:noreply, state}
end
def handle_report({:report_error, []} = code, %{status: :configuration} = state) do
if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
{:stop, {:error, state.current}, state}
end
def handle_report({:report_error, []} = code, state) do
if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
{:noreply, %{state | caller_pid: nil, current: nil}, 0}
end
def handle_report({:report_invalid, []} = code, %{status: :configuration} = state) do
if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
{:stop, {:error, state.current}, state}
end
def handle_report({:report_invalid, []} = code, state) do
if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
{:noreply, %{state | caller_pid: nil, current: nil}, 0}
end
def handle_report({:report_retry, []} = code, %{status: :configuration} = state) do
Logger.warn("Retrying configuration command: #{inspect(code)}")
{:noreply, state}
end
def handle_report({:report_retry, []} = code, state) do
if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
{:noreply, state}
end
def handle_report({:report_paramater_value, param} = code, state) do
if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
side_effects(state, :handle_paramater_value, [param])
{:noreply, state}
end
def handle_report({:report_calibration_paramater_value, args} = _code, state) do
to_process = [{:paramater_write, args}]
{:noreply, goto(%{state | tag: state.tag, configuration_queue: to_process}, :configuration),
0}
end
# report_no_config => goto(_, :no_config)
def handle_report({:report_no_config, []}, %{status: _} = state) do
tag = state.tag || "0"
loaded_params = side_effects(state, :load_params, []) || []
param_commands =
Enum.reduce(loaded_params, [], fn {param, val}, acc ->
if val, do: acc ++ [{:paramater_write, [{param, val}]}], else: acc
end)
to_process =
param_commands ++
[
{:paramater_write, [{:param_config_ok, 1.0}]},
{:paramater_read_all, []}
]
to_process =
if loaded_params[:movement_home_at_boot_x] == 1,
do: to_process ++ [{:command_movement_find_home, [:x]}]
to_process =
if loaded_params[:movement_home_at_boot_y] == 1,
do: to_process ++ [{:command_movement_find_home, [:y]}]
to_process =
if loaded_params[:movement_home_at_boot_z] == 1,
do: to_process ++ [{:command_movement_find_home, [:z]}]
{:noreply, goto(%{state | tag: tag, configuration_queue: to_process}, :configuration), 0}
end
# report_paramaters_complete => goto(:configuration, :idle)
def handle_report({:report_paramaters_complete, []}, %{status: :configuration} = state) do
{:noreply, goto(state, :idle)}
end
def handle_report(_, %{status: :no_config} = state) do
{:noreply, state}
end
def handle_report({:report_position, position} = code, state) do
if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
side_effects(state, :handle_position, [position])
{:noreply, state}
end
def handle_report({:report_axis_state, axis_state} = code, state) do
if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
side_effects(state, :handle_axis_state, [axis_state])
{:noreply, state}
end
def handle_report({:report_calibration_state, calibration_state} = code, state) do
if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
side_effects(state, :handle_calibration_state, [calibration_state])
{:noreply, state}
end
def handle_report({:report_position_change, position} = code, state) do
if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
side_effects(state, :handle_position_change, [position])
{:noreply, state}
end
def handle_report({:report_encoders_scaled, encoders} = code, state) do
if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
side_effects(state, :handle_encoders_scaled, [encoders])
{:noreply, state}
end
def handle_report({:report_encoders_raw, encoders} = code, state) do
if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
side_effects(state, :handle_encoders_raw, [encoders])
{:noreply, state}
end
def handle_report({:report_end_stops, end_stops} = code, state) do
if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
side_effects(state, :handle_end_stops, [end_stops])
{:noreply, state}
end
def handle_report({:report_pin_value, value} = code, state) do
if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
side_effects(state, :handle_pin_value, [value])
{:noreply, state}
end
def handle_report({:report_software_version, version} = code, state) do
if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
side_effects(state, :handle_software_version, [version])
{:noreply, state}
end
# NOOP
def handle_report({:report_echo, _}, state), do: {:noreply, state}
def handle_report({_kind, _args} = code, state) do
IO.inspect(code, label: "unknown code for #{state.status}")
{:noreply, state}
end
@spec goto(state(), status()) :: state()
defp goto(%{status: old} = state, new) do
new_state = %{state | status: new}
cond do
old != new && new == :emergency_lock ->
side_effects(new_state, :handle_emergency_lock, [])
old != new && old == :emergency_lock ->
side_effects(new_state, :handle_emergency_unlock, [])
old == new ->
:ok
true ->
Logger.debug("unhandled state change: #{old} => #{new}")
end
new_state
end
@spec side_effects(state, atom, GCODE.args()) :: any()
defp side_effects(%{side_effects: nil}, _function, _args), do: nil
defp side_effects(%{side_effects: m}, function, args), do: apply(m, function, args)
end

View File

@ -0,0 +1,50 @@
defmodule Farmbot.Firmware.Command do
@moduledoc false
alias Farmbot.{Firmware, Firmware.GCODE}
@spec command(GenServer.server(), GCODE.t() | {GCODE.kind(), GCODE.args()}) ::
:ok | {:error, :invalid_command | :firmware_error | :emergency_lock | Firmware.status()}
def command(firmware_server \\ Firmware, code)
def command(firmware_server, {_tag, {_, _}} = code) do
case GenServer.call(firmware_server, code, :infinity) do
{:ok, tag} -> wait_for_command_result(tag, code)
{:error, status} -> {:error, status}
end
end
def command(firmware_server, {_, _} = code) do
command(firmware_server, {to_string(:rand.uniform(100)), code})
end
defp wait_for_command_result(tag, code, retries \\ 0, err \\ nil) do
receive do
{^tag, {:report_begin, []}} ->
wait_for_command_result(tag, code, retries, err)
{^tag, {:report_busy, []}} ->
wait_for_command_result(tag, code, retries, err)
{^tag, {:report_success, []}} ->
:ok
{^tag, {:report_retry, []}} ->
wait_for_command_result(tag, code, retries + 1, err)
{^tag, {:report_position_change, _} = error} ->
wait_for_command_result(tag, code, retries, error)
{^tag, {:report_error, []}} ->
if err, do: {:error, err}, else: {:error, :firmware_error}
{^tag, {:report_invalid, []}} ->
{:error, :invalid_command}
{_, {:report_emergency_lock, []}} ->
{:error, :emergency_lock}
{_tag, _report} ->
wait_for_command_result(tag, code, retries, err)
end
end
end

View File

@ -0,0 +1,142 @@
defmodule Farmbot.Firmware.GCODE do
@moduledoc """
Handles encoding and decoding of GCODEs.
"""
alias Farmbot.Firmware.GCODE.{Decoder, Encoder}
import Decoder, only: [do_decode: 2]
import Encoder, only: [do_encode: 2]
@typedoc "Tag is a binary integer. example: `\"123\"`"
@type tag() :: nil | binary()
@typedoc "RXX codes. Reports information."
@type report_kind ::
:report_idle
| :report_begin
| :report_success
| :report_error
| :report_busy
| :report_axis_state
| :report_retry
| :report_echo
| :report_invalid
| :report_home_complete
| :report_position
| :report_paramaters_complete
| :report_paramater_value
| :report_calibration_paramater_value
| :report_status_value
| :report_pin_value
| :report_axis_timeout
| :report_end_stops
| :report_software_version
| :report_encoders_scaled
| :report_encoders_raw
| :report_emergency_lock
| :report_no_config
| :report_debug_message
@typedoc "Movement commands"
@type command_kind ::
:command_movement
| :command_movement_home
| :command_movement_find_home
| :command_movement_calibrate
@typedoc "Read/Write commands."
@type read_write_kind ::
:paramater_read_all
| :paramater_read
| :paramater_write
| :status_read
| :status_write
| :pin_read
| :pin_write
| :pin_mode_write
| :servo_write
| :end_stops_read
| :position_read
| :software_version_read
| :position_write_zero
@type emergency_commands :: :command_emergency_lock | :command_emergency_unlock
@typedoc "Kind is an atom of the \"name\" of a command. Example: `:write_paramater`"
@type kind() :: report_kind | command_kind | read_write_kind | :unknown
@typedoc "Args is a list of args to a `kind`. example: `[x: 100.00]`"
@type args() :: [arg]
@typedoc "Example: `{:x, 100.00}` or `1` or `\"hello world\"`"
@type arg() :: any()
@typedoc "Constructed GCODE."
@type t :: {tag(), {kind(), args}}
@doc """
Shortcut for constructing a new GCODE
## Examples
iex(1)> Farmbot.Firmware.GCODE.new(:report_idle, [], "100")
{"100", {:report_idle, []}}
iex(2)> Farmbot.Firmware.GCODE.new(:report_idle, [])
{nil, {:report_idle, []}}
"""
@spec new(kind(), args(), tag()) :: t()
def new(kind, args, tag \\ nil) do
{tag, {kind, args}}
end
@doc """
Takes a string representation of a GCODE, and returns a tuple representation of:
`{tag, {kind, args}}`
## Examples
iex(1)> Farmbot.Firmware.GCODE.decode("R00 Q100")
{"100", {:report_idle, []}}
iex(2)> Farmbot.Firmware.GCODE.decode("R00")
{nil, {:report_idle, []}}
"""
@spec decode(binary()) :: t()
def decode(binary_with_q) when is_binary(binary_with_q) do
code = String.split(binary_with_q, " ")
case extract_tag(code) do
{tag, [kind | args]} ->
{tag, do_decode(kind, args)}
{tag, []} ->
{tag, {:unknown, []}}
end
end
@doc """
Takes a tuple representation of a GCODE and returns a string.
## Examples
iex(1)> Farmbot.Firmware.GCODE.encode({"444", {:report_idle, []}})
"R00 Q444"
iex(2)> Farmbot.Firmware.GCODE.encode({nil, {:report_idle, []}})
"R00"
"""
@spec encode(t()) :: binary()
def encode({nil, {kind, args}}) do
do_encode(kind, args)
end
def encode({tag, {kind, args}}) do
str = do_encode(kind, args)
str <> " Q" <> tag
end
@doc false
@spec extract_tag([binary()]) :: {tag(), [binary()]}
def extract_tag(list) when is_list(list) do
with {"Q" <> bin_tag, list} when is_list(list) <- List.pop_at(list, -1) do
{bin_tag, list}
else
# if there was no Q code provided
{_, data} when is_list(data) -> {nil, list}
end
end
end

View File

@ -0,0 +1,185 @@
defmodule Farmbot.Firmware.GCODE.Decoder do
@moduledoc false
alias Farmbot.Firmware.{GCODE, Param}
@doc false
@spec do_decode(binary(), [binary()]) :: {GCODE.kind(), GCODE.args()}
def do_decode("R00", []), do: {:report_idle, []}
def do_decode("R01", []), do: {:report_begin, []}
def do_decode("R02", []), do: {:report_success, []}
def do_decode("R03", []), do: {:report_error, []}
def do_decode("R04", []), do: {:report_busy, []}
def do_decode("R05", xyz), do: {:report_axis_state, decode_axis_state(xyz)}
def do_decode("R06", xyz), do: {:report_calibration_state, decode_calibration_state(xyz)}
def do_decode("R07", []), do: {:report_retry, []}
def do_decode("R08", args), do: {:report_echo, decode_echo(Enum.join(args, " "))}
def do_decode("R09", []), do: {:report_invalid, []}
def do_decode("R11", []), do: {:report_home_complete, [:x]}
def do_decode("R12", []), do: {:report_home_complete, [:y]}
def do_decode("R13", []), do: {:report_home_complete, [:z]}
def do_decode("R15", x), do: {:report_position_change, decode_floats(x)}
def do_decode("R16", y), do: {:report_position_change, decode_floats(y)}
def do_decode("R17", z), do: {:report_position_change, decode_floats(z)}
def do_decode("R20", []), do: {:report_paramaters_complete, []}
def do_decode("R21", pv), do: {:report_paramater_value, decode_pv(pv)}
def do_decode("R23", pv), do: {:report_calibration_paramater_value, decode_pv(pv)}
def do_decode("R41", pv), do: {:report_pin_value, decode_ints(pv)}
def do_decode("R71", []), do: {:report_axis_timeout, [:x]}
def do_decode("R72", []), do: {:report_axis_timeout, [:y]}
def do_decode("R73", []), do: {:report_axis_timeout, [:z]}
def do_decode("R81", xxyyzz), do: {:report_end_stops, decode_end_stops(xxyyzz)}
def do_decode("R82", xyzs), do: {:report_position, decode_floats(xyzs)}
def do_decode("R83", [version]), do: {:report_software_version, [version]}
def do_decode("R84", xyz), do: {:report_encoders_scaled, decode_floats(xyz)}
def do_decode("R85", xyz), do: {:report_encoders_raw, decode_floats(xyz)}
def do_decode("R87", []), do: {:report_emergency_lock, []}
def do_decode("R88", []), do: {:report_no_config, []}
def do_decode("R99", debug), do: {:report_debug_message, [Enum.join(debug, " ")]}
def do_decode("G00", xyzs), do: {:command_movement, decode_floats(xyzs)}
def do_decode("G28", []), do: {:comand_movement_home, [:x, :y, :z]}
def do_decode("F11", []), do: {:command_movement_find_home, [:x]}
def do_decode("F12", []), do: {:command_movement_find_home, [:y]}
def do_decode("F13", []), do: {:command_movement_find_home, [:z]}
def do_decode("F14", []), do: {:command_movement_calibrate, [:x]}
def do_decode("F15", []), do: {:command_movement_calibrate, [:y]}
def do_decode("F16", []), do: {:command_movement_calibrate, [:z]}
def do_decode("F20", []), do: {:paramater_read_all, []}
def do_decode("F21", [param_id]), do: {:paramater_read, [Param.decode(param_id)]}
def do_decode("F22", pv), do: {:paramater_write, decode_pv(pv)}
def do_decode("F23", pv), do: {:calibration_paramater_write, decode_pv(pv)}
def do_decode("F41", pvm), do: {:pin_write, decode_ints(pvm)}
def do_decode("F42", pv), do: {:pin_read, decode_ints(pv)}
def do_decode("F43", pm), do: {:pin_mode_write, decode_ints(pm)}
def do_decode("F61", pv), do: {:servo_write, decode_ints(pv)}
def do_decode("F81", []), do: {:end_stops_read, []}
def do_decode("F82", []), do: {:position_read, []}
def do_decode("F83", []), do: {:software_version_read, []}
def do_decode("F84", xyzs), do: {:position_write_zero, decode_ints(xyzs)}
def do_decode("F09", _), do: {:command_emergency_unlock, []}
def do_decode("E", _), do: {:command_emergency_lock, []}
def do_decode(kind, args) do
{:unknown, [kind | args]}
end
defp decode_floats(list, acc \\ [])
defp decode_floats([<<arg::binary-1, val::binary>> | rest], acc) do
arg =
arg
|> String.downcase()
|> String.to_existing_atom()
case Float.parse(val) do
{num, ""} ->
decode_floats(rest, Keyword.put(acc, arg, num))
_ ->
case Integer.parse(val) do
{num, ""} -> decode_floats(rest, Keyword.put(acc, arg, num / 1))
_ -> decode_floats(rest, acc)
end
end
end
# This is sort of order dependent and not exactly correct.
# It should ensure the order is [x: _, y: _, z: _]
defp decode_floats([], acc), do: Enum.reverse(acc)
defp decode_axis_state(list) do
args = decode_floats(list)
Enum.map(args, fn {axis, value} ->
case value do
0.0 -> {axis, :idle}
1.0 -> {axis, :begin}
2.0 -> {axis, :accelerate}
3.0 -> {axis, :cruise}
4.0 -> {axis, :decelerate}
5.0 -> {axis, :stop}
6.0 -> {axis, :crawl}
end
end)
end
defp decode_calibration_state(list) do
args = decode_floats(list)
Enum.map(args, fn {axis, value} ->
case value do
0.0 -> {axis, :idle}
1.0 -> {axis, :home}
2.0 -> {axis, :end}
end
end)
end
@spec decode_end_stops([binary()], Keyword.t()) :: Keyword.t()
defp decode_end_stops(list, acc \\ [])
defp decode_end_stops(
[<<arg::binary-1, "A", val0::binary>>, <<arg::binary-1, "B", val1::binary>> | rest],
acc
) do
dc = String.downcase(arg)
acc =
acc ++
[
{:"#{dc}a", String.to_integer(val0)},
{:"#{dc}b", String.to_integer(val1)}
]
decode_end_stops(rest, acc)
end
defp decode_end_stops([], acc), do: acc
defp decode_pv(["P" <> param_id, "V" <> value]) do
param = Param.decode(String.to_integer(param_id))
{value, ""} = Float.parse(value)
[{param, value}]
end
defp decode_ints(pvm, acc \\ [])
defp decode_ints([<<arg::binary-1, val::binary>> | rest], acc) do
arg =
arg
|> String.downcase()
|> String.to_existing_atom()
case Integer.parse(val) do
{num, ""} -> decode_ints(rest, Keyword.put(acc, arg, num))
_ -> decode_ints(rest, acc)
end
end
defp decode_ints([], acc), do: Enum.reverse(acc)
@spec decode_echo(binary()) :: [binary()]
defp decode_echo(str) when is_binary(str) do
[_, echo | _] = String.split(str, "*", parts: 3)
[String.trim(echo)]
end
end

View File

@ -0,0 +1,142 @@
defmodule Farmbot.Firmware.GCODE.Encoder do
@moduledoc false
alias Farmbot.Firmware.{GCODE, Param}
@doc false
@spec do_encode(GCODE.kind(), GCODE.args()) :: binary()
def do_encode(:report_idle, []), do: "R00"
def do_encode(:report_begin, []), do: "R01"
def do_encode(:report_success, []), do: "R02"
def do_encode(:report_error, []), do: "R03"
def do_encode(:report_busy, []), do: "R04"
def do_encode(:report_axis_state, xyz), do: "R05 " <> encode_axis_state(xyz)
def do_encode(:report_calibration_state, xyz), do: "R06 " <> encode_calibration_state(xyz)
def do_encode(:report_retry, []), do: "R07"
def do_encode(:report_echo, [echo]), do: "R08 * #{echo} *"
def do_encode(:report_invalid, []), do: "R09"
def do_encode(:report_home_complete, [:x]), do: "R11"
def do_encode(:report_home_complete, [:y]), do: "R12"
def do_encode(:report_home_complete, [:z]), do: "R13"
def do_encode(:report_position_change, [x: _] = arg), do: "R15 " <> encode_floats(arg)
def do_encode(:report_position_change, [y: _] = arg), do: "R16 " <> encode_floats(arg)
def do_encode(:report_position_change, [z: _] = arg), do: "R16 " <> encode_floats(arg)
def do_encode(:report_paramaters_complete, []), do: "R20"
def do_encode(:report_parmater_value, pv), do: "R21 " <> encode_pv(pv)
def do_encode(:report_calibration_paramater_value, pv), do: "R23 " <> encode_pv(pv)
def do_encode(:report_pin_value, pv), do: "R41 " <> encode_ints(pv)
def do_encode(:report_axis_timeout, [:x]), do: "R71"
def do_encode(:report_axis_timeout, [:y]), do: "R72"
def do_encode(:report_axis_timeout, [:z]), do: "R73"
def do_encode(:report_end_stops, xxyyzz), do: "R81 " <> encode_end_stops(xxyyzz)
def do_encode(:report_position, xyzs), do: "R82 " <> encode_floats(xyzs)
def do_encode(:report_software_version, [version]), do: "R83 " <> version
def do_encode(:report_encoders_scaled, xyz), do: "R84 " <> encode_floats(xyz)
def do_encode(:report_encoders_raw, xyz), do: "R85 " <> encode_floats(xyz)
def do_encode(:report_emergency_lock, []), do: "R87"
def do_encode(:report_no_config, []), do: "R88"
def do_encode(:report_debug_message, [message]), do: "R99 " <> message
def do_encode(:command_movement, xyzs), do: "G00 " <> encode_floats(xyzs)
def do_encode(:command_movement_home, [:x, :y, :z]), do: "G28"
def do_encode(:command_movement_home, [:x]), do: "G00 " <> encode_floats(x: 0.0)
def do_encode(:command_movement_home, [:y]), do: "G00 " <> encode_floats(y: 0.0)
def do_encode(:command_movement_home, [:z]), do: "G00 " <> encode_floats(z: 0.0)
def do_encode(:command_movement_find_home, [:x]), do: "F11"
def do_encode(:command_movement_find_home, [:y]), do: "F12"
def do_encode(:command_movement_find_home, [:z]), do: "F13"
def do_encode(:command_movement_calibrate, [:x]), do: "F14"
def do_encode(:command_movement_calibrate, [:y]), do: "F15"
def do_encode(:command_movement_calibrate, [:z]), do: "F16"
def do_encode(:paramater_read_all, []), do: "F20"
def do_encode(:paramater_read, [paramater]), do: "F21 P#{Param.encode(paramater)}"
def do_encode(:paramater_write, pv), do: "F22 " <> encode_pv(pv)
def do_encode(:calibration_paramater_write, pv), do: "F23 " <> encode_pv(pv)
def do_encode(:pin_write, pv), do: "F41 " <> encode_ints(pv)
def do_encode(:pin_read, p), do: "F42 " <> encode_ints(p)
def do_encode(:pin_mode_write, pm), do: "F43 " <> encode_ints(pm)
def do_encode(:servo_write, pv), do: "F61 " <> encode_ints(pv)
def do_encode(:end_stops_read, []), do: "F81"
def do_encode(:position_read, []), do: "F82"
def do_encode(:software_version_read, []), do: "F83"
def do_encode(:position_write_zero, [:x, :y, :z]), do: "F84 X1 Y1 Z1"
def do_encode(:position_write_zero, [:x]), do: "F84 X1"
def do_encode(:position_write_zero, [:y]), do: "F84 Y1"
def do_encode(:position_write_zero, [:z]), do: "F84 Z1"
def do_encode(:command_emergency_unlock, _), do: "F09"
def do_encode(:command_emergency_lock, _), do: "E"
@spec encode_floats([{Param.t(), float()}]) :: binary()
defp encode_floats(args) do
Enum.map(args, fn {param, value} ->
binary_float = :erlang.float_to_binary(value, decimals: 2)
String.upcase(to_string(param)) <> binary_float
end)
|> Enum.join(" ")
end
defp encode_axis_state([{axis, :idle}]),
do: String.upcase(to_string(axis)) <> "0"
defp encode_axis_state([{axis, :begin}]),
do: String.upcase(to_string(axis)) <> "1"
defp encode_axis_state([{axis, :accelerate}]),
do: String.upcase(to_string(axis)) <> "2"
defp encode_axis_state([{axis, :cruise}]),
do: String.upcase(to_string(axis)) <> "3"
defp encode_axis_state([{axis, :decelerate}]),
do: String.upcase(to_string(axis)) <> "4"
defp encode_axis_state([{axis, :stop}]),
do: String.upcase(to_string(axis)) <> "5"
defp encode_axis_state([{axis, :crawl}]),
do: String.upcase(to_string(axis)) <> "6"
defp encode_calibration_state([{axis, :idle}]),
do: String.upcase(to_string(axis)) <> "0"
defp encode_calibration_state([{axis, :home}]),
do: String.upcase(to_string(axis)) <> "1"
defp encode_calibration_state([{axis, :end}]),
do: String.upcase(to_string(axis)) <> "2"
defp encode_end_stops(xa: xa, xb: xb, ya: ya, yb: yb, za: za, zb: zb) do
"XA#{xa} XB#{xb} YA#{ya} YB#{yb} ZA#{za} ZB#{zb}"
end
defp encode_pv([{param, value}]) do
param_id = Param.encode(param)
binary_float = :erlang.float_to_binary(value, decimals: 2)
"P#{param_id} V#{binary_float}"
end
defp encode_ints(args) do
Enum.map(args, fn {key, val} ->
String.upcase(to_string(key)) <> to_string(val)
end)
|> Enum.join(" ")
end
end

View File

@ -0,0 +1,197 @@
defmodule Farmbot.Firmware.Param do
@moduledoc "decodes/encodes integer id to name and vice versa"
require Logger
@type t() :: atom()
@doc "Decodes an integer paramater id to a atom paramater name"
def decode(paramater_id)
def decode(2), do: :param_config_ok
def decode(3), do: :param_use_eeprom
def decode(4), do: :param_e_stop_on_mov_err
def decode(5), do: :param_mov_nr_retry
def decode(11), do: :movement_timeout_x
def decode(12), do: :movement_timeout_y
def decode(13), do: :movement_timeout_z
def decode(15), do: :movement_keep_active_x
def decode(16), do: :movement_keep_active_y
def decode(17), do: :movement_keep_active_z
def decode(18), do: :movement_home_at_boot_x
def decode(19), do: :movement_home_at_boot_y
def decode(20), do: :movement_home_at_boot_z
def decode(21), do: :movement_invert_endpoints_x
def decode(22), do: :movement_invert_endpoints_y
def decode(23), do: :movement_invert_endpoints_z
def decode(25), do: :movement_enable_endpoints_x
def decode(26), do: :movement_enable_endpoints_y
def decode(27), do: :movement_enable_endpoints_z
def decode(31), do: :movement_invert_motor_x
def decode(32), do: :movement_invert_motor_y
def decode(33), do: :movement_invert_motor_z
def decode(36), do: :movement_secondary_motor_x
def decode(37), do: :movement_secondary_motor_invert_x
def decode(41), do: :movement_steps_acc_dec_x
def decode(42), do: :movement_steps_acc_dec_y
def decode(43), do: :movement_steps_acc_dec_z
def decode(45), do: :movement_stop_at_home_x
def decode(46), do: :movement_stop_at_home_y
def decode(47), do: :movement_stop_at_home_z
def decode(51), do: :movement_home_up_x
def decode(52), do: :movement_home_up_y
def decode(53), do: :movement_home_up_z
def decode(55), do: :movement_step_per_mm_x
def decode(56), do: :movement_step_per_mm_y
def decode(57), do: :movement_step_per_mm_z
def decode(61), do: :movement_min_spd_x
def decode(62), do: :movement_min_spd_y
def decode(63), do: :movement_min_spd_z
def decode(65), do: :movement_home_spd_x
def decode(66), do: :movement_home_spd_y
def decode(67), do: :movement_home_spd_z
def decode(71), do: :movement_max_spd_x
def decode(72), do: :movement_max_spd_y
def decode(73), do: :movement_max_spd_z
def decode(75), do: :movement_invert_2_endpoints_x
def decode(76), do: :movement_invert_2_endpoints_y
def decode(77), do: :movement_invert_2_endpoints_z
def decode(101), do: :encoder_enabled_x
def decode(102), do: :encoder_enabled_y
def decode(103), do: :encoder_enabled_z
def decode(105), do: :encoder_type_x
def decode(106), do: :encoder_type_y
def decode(107), do: :encoder_type_z
def decode(111), do: :encoder_missed_steps_max_x
def decode(112), do: :encoder_missed_steps_max_y
def decode(113), do: :encoder_missed_steps_max_z
def decode(115), do: :encoder_scaling_x
def decode(116), do: :encoder_scaling_y
def decode(117), do: :encoder_scaling_z
def decode(121), do: :encoder_missed_steps_decay_x
def decode(122), do: :encoder_missed_steps_decay_y
def decode(123), do: :encoder_missed_steps_decay_z
def decode(125), do: :encoder_use_for_pos_x
def decode(126), do: :encoder_use_for_pos_y
def decode(127), do: :encoder_use_for_pos_z
def decode(131), do: :encoder_invert_x
def decode(132), do: :encoder_invert_y
def decode(133), do: :encoder_invert_z
def decode(141), do: :movement_axis_nr_steps_x
def decode(142), do: :movement_axis_nr_steps_y
def decode(143), do: :movement_axis_nr_steps_z
def decode(145), do: :movement_stop_at_max_x
def decode(146), do: :movement_stop_at_max_y
def decode(147), do: :movement_stop_at_max_z
def decode(201), do: :pin_guard_1_pin_nr
def decode(202), do: :pin_guard_1_time_out
def decode(203), do: :pin_guard_1_active_state
def decode(205), do: :pin_guard_2_pin_nr
def decode(206), do: :pin_guard_2_time_out
def decode(207), do: :pin_guard_2_active_state
def decode(211), do: :pin_guard_3_pin_nr
def decode(212), do: :pin_guard_3_time_out
def decode(213), do: :pin_guard_3_active_state
def decode(215), do: :pin_guard_4_pin_nr
def decode(216), do: :pin_guard_4_time_out
def decode(217), do: :pin_guard_4_active_state
def decode(221), do: :pin_guard_5_pin_nr
def decode(222), do: :pin_guard_5_time_out
def decode(223), do: :pin_guard_5_active_state
def decode(unknown) when is_integer(unknown) do
Logger.error("unknown firmware paramater: #{unknown}")
:unknown_paramater
end
@doc "Encodes an atom paramater name to an integer paramater id."
def encode(paramater)
def encode(:param_config_ok), do: 2
def encode(:param_use_eeprom), do: 3
def encode(:param_e_stop_on_mov_err), do: 4
def encode(:param_mov_nr_retry), do: 5
def encode(:movement_timeout_x), do: 11
def encode(:movement_timeout_y), do: 12
def encode(:movement_timeout_z), do: 13
def encode(:movement_keep_active_x), do: 15
def encode(:movement_keep_active_y), do: 16
def encode(:movement_keep_active_z), do: 17
def encode(:movement_home_at_boot_x), do: 18
def encode(:movement_home_at_boot_y), do: 19
def encode(:movement_home_at_boot_z), do: 20
def encode(:movement_invert_endpoints_x), do: 21
def encode(:movement_invert_endpoints_y), do: 22
def encode(:movement_invert_endpoints_z), do: 23
def encode(:movement_enable_endpoints_x), do: 25
def encode(:movement_enable_endpoints_y), do: 26
def encode(:movement_enable_endpoints_z), do: 27
def encode(:movement_invert_motor_x), do: 31
def encode(:movement_invert_motor_y), do: 32
def encode(:movement_invert_motor_z), do: 33
def encode(:movement_secondary_motor_x), do: 36
def encode(:movement_secondary_motor_invert_x), do: 37
def encode(:movement_steps_acc_dec_x), do: 41
def encode(:movement_steps_acc_dec_y), do: 42
def encode(:movement_steps_acc_dec_z), do: 43
def encode(:movement_stop_at_home_x), do: 45
def encode(:movement_stop_at_home_y), do: 46
def encode(:movement_stop_at_home_z), do: 47
def encode(:movement_home_up_x), do: 51
def encode(:movement_home_up_y), do: 52
def encode(:movement_home_up_z), do: 53
def encode(:movement_step_per_mm_x), do: 55
def encode(:movement_step_per_mm_y), do: 56
def encode(:movement_step_per_mm_z), do: 57
def encode(:movement_min_spd_x), do: 61
def encode(:movement_min_spd_y), do: 62
def encode(:movement_min_spd_z), do: 63
def encode(:movement_home_spd_x), do: 65
def encode(:movement_home_spd_y), do: 66
def encode(:movement_home_spd_z), do: 67
def encode(:movement_max_spd_x), do: 71
def encode(:movement_max_spd_y), do: 72
def encode(:movement_max_spd_z), do: 73
def encode(:movement_invert_2_endpoints_x), do: 75
def encode(:movement_invert_2_endpoints_y), do: 76
def encode(:movement_invert_2_endpoints_z), do: 77
def encode(:encoder_enabled_x), do: 101
def encode(:encoder_enabled_y), do: 102
def encode(:encoder_enabled_z), do: 103
def encode(:encoder_type_x), do: 105
def encode(:encoder_type_y), do: 106
def encode(:encoder_type_z), do: 107
def encode(:encoder_missed_steps_max_x), do: 111
def encode(:encoder_missed_steps_max_y), do: 112
def encode(:encoder_missed_steps_max_z), do: 113
def encode(:encoder_scaling_x), do: 115
def encode(:encoder_scaling_y), do: 116
def encode(:encoder_scaling_z), do: 117
def encode(:encoder_missed_steps_decay_x), do: 121
def encode(:encoder_missed_steps_decay_y), do: 122
def encode(:encoder_missed_steps_decay_z), do: 123
def encode(:encoder_use_for_pos_x), do: 125
def encode(:encoder_use_for_pos_y), do: 126
def encode(:encoder_use_for_pos_z), do: 127
def encode(:encoder_invert_x), do: 131
def encode(:encoder_invert_y), do: 132
def encode(:encoder_invert_z), do: 133
def encode(:movement_axis_nr_steps_x), do: 141
def encode(:movement_axis_nr_steps_y), do: 142
def encode(:movement_axis_nr_steps_z), do: 143
def encode(:movement_stop_at_max_x), do: 145
def encode(:movement_stop_at_max_y), do: 146
def encode(:movement_stop_at_max_z), do: 147
def encode(:pin_guard_1_pin_nr), do: 201
def encode(:pin_guard_1_time_out), do: 202
def encode(:pin_guard_1_active_state), do: 203
def encode(:pin_guard_2_pin_nr), do: 205
def encode(:pin_guard_2_time_out), do: 206
def encode(:pin_guard_2_active_state), do: 207
def encode(:pin_guard_3_pin_nr), do: 211
def encode(:pin_guard_3_time_out), do: 212
def encode(:pin_guard_3_active_state), do: 213
def encode(:pin_guard_4_pin_nr), do: 215
def encode(:pin_guard_4_time_out), do: 216
def encode(:pin_guard_4_active_state), do: 217
def encode(:pin_guard_5_pin_nr), do: 221
def encode(:pin_guard_5_time_out), do: 222
def encode(:pin_guard_5_active_state), do: 223
end

View File

@ -0,0 +1,139 @@
defmodule Farmbot.Firmware.Request do
@moduledoc false
alias Farmbot.{Firmware, Firmware.GCODE}
@spec request(GenServer.server(), GCODE.t()) ::
{:ok, GCODE.t()} | {:error, :invalid_command | :firmware_error | Firmware.status()}
def request(firmware_server \\ Firmware, code)
def request(firmware_server, {_tag, {kind, _}} = code) do
if kind not in [
:paramater_read,
:status_read,
:pin_read,
:end_stops_read,
:position_read,
:software_version_read
] do
raise ArgumentError, "#{kind} is not a valid request."
end
case GenServer.call(firmware_server, code, :infinity) do
{:ok, tag} -> wait_for_request_result(tag, code)
{:error, status} -> {:error, status}
end
end
def request(firmware_server, {_, _} = code) do
request(firmware_server, {to_string(:rand.uniform(100)), code})
end
# This is a bit weird but let me explain:
# if this function `receive`s
# * report_error
# * report_invalid
# * report_emergency_lock
# it needs to return an error.
# If this function `receive`s
# * report_success
# when no valid data has been collected from `wait_for_request_result_process`
# it needs to return an error.
# If this function `receive`s
# * report_success
# when valid data has been collected from `wait_for_request_result_process`
# it will return that data.
# If this function returns no data for 5 seconds, it needs to error.
defp wait_for_request_result(tag, code, result \\ nil) do
receive do
{^tag, {:report_begin, []}} ->
wait_for_request_result(tag, code, result)
{^tag, {:report_busy, []}} ->
wait_for_request_result(tag, code, result)
{^tag, {:report_success, []}} ->
if result,
do: {:ok, {tag, result}},
else: wait_for_request_result(tag, code, result)
{^tag, {:report_error, []}} ->
{:error, :firmware_error}
{^tag, {:report_invalid, []}} ->
{:error, :invalid_command}
{_, {:report_emergency_lock, []}} ->
{:error, :emergency_lock}
{_tag, report} ->
wait_for_request_result_process(report, tag, code, result)
after
5_000 ->
if result, do: {:ok, {tag, result}}, else: {:error, {:timeout, result}}
end
end
# {:paramater_read, [param]} => {:report_paramater_value, [{param, val}]}
defp wait_for_request_result_process(
{:report_paramater_value, _} = report,
tag,
{_, {:paramater_read, _}} = code,
_
) do
wait_for_request_result(tag, code, report)
end
# {:status_read, [status]} => {:report_status_value, [{status, value}]}
defp wait_for_request_result_process(
{:report_status_value, _} = report,
tag,
{_, {:status_read, _}} = code,
_
) do
wait_for_request_result(tag, code, report)
end
# {:pin_read, [pin]} => {:report_pin_value, [{pin, value}]}
defp wait_for_request_result_process(
{:report_pin_value, _} = report,
tag,
{_, {:pin_read, _}} = code,
_
) do
wait_for_request_result(tag, code, report)
end
# {:end_stops_read, []} => {:position_end_stops, end_stops}
defp wait_for_request_result_process(
{:report_end_stops, _} = report,
tag,
{_, {:end_stops_read, []}} = code,
_
) do
wait_for_request_result(tag, code, report)
end
# {:position_read, []} => {:position_report, [x: x, y: y, z: z]}
defp wait_for_request_result_process(
{:report_position, _} = report,
tag,
{_, {:position_read, []}} = code,
_
) do
wait_for_request_result(tag, code, report)
end
# {:software_version_read, []} => {:report_software_version, [version]}
defp wait_for_request_result_process(
{:report_software_version, _} = report,
tag,
{_, {:software_version_read, _}} = code,
_
) do
wait_for_request_result(tag, code, report)
end
defp wait_for_request_result_process(_report, tag, code, result) do
wait_for_request_result(tag, code, result)
end
end

View File

@ -0,0 +1,30 @@
defmodule Farmbot.Firmware.SideEffects do
alias Farmbot.Firmware.{GCODE, Param}
@type axis :: :x | :y | :z
@doc "While in state `:boot`, the firmware needs to load its params."
@callback load_params :: [{Param.t(), float() | nil}]
@callback handle_position(x: float(), y: float(), z: float()) :: any()
@callback handle_position_change([{axis(), float()}]) :: any()
@callback handle_encoders_scaled(x: float(), y: float(), z: float()) :: any()
@callback handle_encoders_raw(x: float(), y: float(), z: float()) :: any()
@callback handle_paramater_value([{Param.t(), float()}]) :: any()
@callback handle_end_stops(xa: 0 | 1, xb: 0 | 1, ya: 0 | 1, yb: 0 | 1, za: 0 | 1, zb: 0 | 1) ::
any()
@callback handle_emergency_lock() :: any()
@callback handle_emergency_unlock() :: any()
@callback handle_pin_value(p: integer(), v: integer()) :: any()
@callback handle_software_version([String.t()]) :: any()
@type axis_state :: :stop | :idle | :begin | :crawl | :decelerate | :accelerate
@callback handle_axis_state([{axis(), axis_state}]) :: any()
@type calibration_state :: :idle | :home | :end
@callback handle_calibration_state([{axis(), calibration_state()}]) :: any()
@callback handle_input_gcode(GCODE.t()) :: any()
@callback handle_output_gcode(GCODE.t()) :: any()
@callback handle_debug_message([String.t()]) :: any()
end

View File

@ -0,0 +1,126 @@
defmodule Farmbot.Firmware.StubSideEffects do
@behaviour Farmbot.Firmware.SideEffects
def load_params do
[
movement_home_spd_z: 200.0,
movement_step_per_mm_x: 5.0,
movement_min_spd_z: 200.0,
movement_home_at_boot_z: 1.0,
pin_guard_5_active_state: 1.0,
encoder_missed_steps_decay_z: 5.0,
movement_step_per_mm_y: 5.0,
pin_guard_1_active_state: 1.0,
movement_max_spd_z: 400.0,
movement_invert_2_endpoints_y: 0.0,
movement_home_up_y: 0.0,
pin_guard_5_pin_nr: 0.0,
encoder_use_for_pos_y: 1.0,
encoder_enabled_z: 1.0,
encoder_use_for_pos_z: 1.0,
movement_home_up_x: 0.0,
encoder_missed_steps_max_z: 5.0,
pin_guard_3_active_state: 1.0,
movement_keep_active_y: 0.0,
movement_timeout_z: 120.0,
encoder_invert_x: 0.0,
movement_home_spd_y: 400.0,
param_e_stop_on_mov_err: 0.0,
pin_guard_4_pin_nr: 0.0,
movement_axis_nr_steps_z: 7050.0,
movement_steps_acc_dec_x: 100.0,
movement_invert_motor_z: 0.0,
encoder_scaling_x: 5556.0,
movement_home_spd_x: 400.0,
movement_keep_active_x: 0.0,
movement_enable_endpoints_z: 0.0,
movement_invert_endpoints_y: 0.0,
encoder_missed_steps_max_x: 5.0,
movement_stop_at_max_x: 1.0,
pin_guard_4_active_state: 1.0,
movement_secondary_motor_x: 1.0,
encoder_invert_y: 0.0,
movement_axis_nr_steps_y: 1686.0,
movement_invert_2_endpoints_z: 0.0,
movement_timeout_x: 120.0,
encoder_missed_steps_max_y: 5.0,
movement_stop_at_home_x: 1.0,
pin_guard_4_time_out: 60.0,
movement_secondary_motor_invert_x: 1.0,
movement_invert_endpoints_z: 0.0,
movement_steps_acc_dec_y: 100.0,
encoder_invert_z: 0.0,
movement_home_at_boot_y: 1.0,
encoder_scaling_y: 5556.0,
movement_invert_2_endpoints_x: 0.0,
movement_steps_acc_dec_z: 300.0,
encoder_type_z: 0.0,
encoder_type_y: 0.0,
encoder_use_for_pos_x: 1.0,
movement_enable_endpoints_y: 0.0,
movement_invert_endpoints_x: 0.0,
pin_guard_2_active_state: 1.0,
movement_invert_motor_x: 0.0,
movement_keep_active_z: 1.0,
movement_stop_at_max_z: 1.0,
pin_guard_5_time_out: 60.0,
movement_min_spd_x: 250.0,
movement_timeout_y: 120.0,
encoder_missed_steps_decay_y: 5.0,
movement_max_spd_x: 800.0,
encoder_enabled_x: 1.0,
pin_guard_1_pin_nr: 0.0,
movement_home_at_boot_x: 1.0,
movement_min_spd_y: 350.0,
movement_invert_motor_y: 0.0,
param_mov_nr_retry: 3.0,
pin_guard_2_pin_nr: 0.0,
movement_home_up_z: 1.0,
movement_axis_nr_steps_x: 1342.0,
encoder_enabled_y: 1.0,
movement_stop_at_max_y: 1.0,
movement_stop_at_home_z: 1.0,
movement_step_per_mm_z: 25.0,
pin_guard_3_time_out: 60.0,
encoder_type_x: 0.0,
pin_guard_1_time_out: 60.0,
movement_enable_endpoints_x: 0.0,
movement_max_spd_y: 800.0,
pin_guard_3_pin_nr: 0.0,
movement_stop_at_home_y: 1.0,
pin_guard_2_time_out: 60.0,
encoder_scaling_z: 5556.0,
encoder_missed_steps_decay_x: 5.0
]
end
def handle_position(_), do: :noop
def handle_position_change(_), do: :noop
def handle_axis_state(_), do: :noop
def handle_calibration_state(_), do: :noop
def handle_encoders_scaled(_), do: :noop
def handle_encoders_raw(_), do: :noop
def handle_paramater_value(_), do: :noop
def handle_end_stops(_), do: :noop
def handle_emergency_lock(), do: :noop
def handle_emergency_unlock(), do: :noop
def handle_pin_value(_), do: :noop
def handle_software_version(_), do: :noop
def handle_input_gcode(_), do: :noop
def handle_output_gcode(_), do: :noop
def handle_debug_message(_), do: :noop
end

View File

@ -0,0 +1,356 @@
defmodule Farmbot.Firmware.StubTransport do
@moduledoc "Stub for transporting GCODES. Simulates the _real_ Firmware."
use GenServer
alias Farmbot.Firmware.StubTransport, as: State
alias Farmbot.Firmware.{GCODE, Param}
require Logger
defstruct status: :boot,
handle_gcode: nil,
position: [x: 0.0, y: 0.0, z: 0.0],
encoders_scaled: [x: 0.0, y: 0.0, z: 0.0],
encoders_raw: [x: 0.0, y: 0.0, z: 0.0],
pins: %{},
params: []
@type t :: %State{
status: Farmbot.Firmware.status(),
handle_gcode: (Farmbot.Firmware.GCODE.t() -> :ok),
position: [x: float(), y: float(), z: float()],
encoders_scaled: [x: float(), y: float(), z: float()],
encoders_raw: [x: float(), y: float(), z: float()],
pins: %{},
params: [{Param.t(), float() | nil}]
}
def init(args) do
handle_gcode = Keyword.fetch!(args, :handle_gcode)
{:ok, %State{status: :boot, handle_gcode: handle_gcode}, 0}
end
def handle_info(:timeout, %{status: :boot} = state) do
state.handle_gcode.(GCODE.new(:report_debug_message, ["ARDUINO STARTUP COMPLETE"]))
{:noreply, goto(state, :no_config), 0}
end
def handle_info(:timeout, %{status: :no_config} = state) do
state.handle_gcode.(GCODE.new(:report_no_config, []))
{:noreply, state}
end
def handle_info(:timeout, %{status: :emergency_lock} = state) do
resp_codes = [
GCODE.new(:report_emergency_lock, [])
]
{:noreply, state, {:continue, resp_codes}}
end
def handle_info(:timeout, %{status: :idle} = state) do
resp_codes = [
GCODE.new(:report_position, state.position),
GCODE.new(:report_encoders_scaled, state.encoders_scaled),
GCODE.new(:report_encoders_raw, state.encoders_raw),
GCODE.new(:report_idle, [])
]
{:noreply, state, {:continue, resp_codes}}
end
def handle_call({tag, {:command_emergency_lock, _}} = code, _from, state) do
resp_codes = [
GCODE.new(:report_echo, [GCODE.encode(code)]),
GCODE.new(:report_begin, [], tag),
GCODE.new(:report_emergency_lock, [], tag)
]
{:reply, :ok, %{state | status: :emergency_lock}, {:continue, resp_codes}}
end
def handle_call({tag, {:command_emergency_unlock, _}} = code, _from, state) do
resp_codes = [
GCODE.new(:report_echo, [GCODE.encode(code)]),
GCODE.new(:report_begin, [], tag),
GCODE.new(:report_success, [], tag)
]
{:reply, :ok, %{state | status: :idle}, {:continue, resp_codes}}
end
def handle_call(
{tag, {:paramater_write, [{:param_config_ok = param, 1.0 = value}]}} = code,
_from,
state
) do
new_state = %{state | params: Keyword.put(state.params, param, value)}
resp_codes = [
GCODE.new(:report_echo, [GCODE.encode(code)]),
GCODE.new(:report_begin, [], tag),
GCODE.new(:report_success, [], tag)
]
{:reply, :ok, goto(new_state, :idle), {:continue, resp_codes}}
end
def handle_call({tag, {:paramater_write, [{param, value}]}} = code, _from, state) do
new_state = %{state | params: Keyword.put(state.params, param, value)}
resp_codes = [
GCODE.new(:report_echo, [GCODE.encode(code)]),
GCODE.new(:report_begin, [], tag),
GCODE.new(:report_success, [], tag)
]
{:reply, :ok, new_state, {:continue, resp_codes}}
end
def handle_call({tag, {:paramater_read_all, []}} = code, _from, state) do
resp_codes =
[
GCODE.new(:report_echo, [GCODE.encode(code)]),
GCODE.new(:report_begin, [], tag),
Enum.map(state.params, fn {p, v} ->
GCODE.new(:report_paramater_value, [{p, v}])
end),
GCODE.new(:report_success, [], tag)
]
|> List.flatten()
{:reply, :ok, state, {:continue, resp_codes}}
end
def handle_call({tag, {:paramater_read, [param]}} = code, _from, state) do
resp_codes = [
GCODE.new(:report_echo, [GCODE.encode(code)]),
GCODE.new(:report_begin, [], tag),
GCODE.new(:report_paramater_value, [{param, state.params[param] || -1.0}]),
GCODE.new(:report_success, [], tag)
]
{:reply, :ok, state, {:continue, resp_codes}}
end
def handle_call({tag, {:position_write_zero, [:x, :y, :z]}} = code, _from, state) do
position = [
x: 0.0,
y: 0.0,
z: 0.0
]
state = %{state | position: position}
resp_codes = [
GCODE.new(:report_echo, [GCODE.encode(code)]),
GCODE.new(:report_begin, [], tag),
GCODE.new(:report_busy, [], tag),
GCODE.new(:report_position, state.position),
GCODE.new(:report_success, [], tag)
]
{:reply, :ok, state, {:continue, resp_codes}}
end
def handle_call({tag, {:position_write_zero, [axis]}} = code, _from, state) do
position = Keyword.put(state.position, axis, 0.0) |> ensure_order()
state = %{state | position: position}
resp_codes = [
GCODE.new(:report_echo, [GCODE.encode(code)]),
GCODE.new(:report_begin, [], tag),
GCODE.new(:report_position, state.position),
GCODE.new(:report_success, [], tag)
]
{:reply, :ok, state, {:continue, resp_codes}}
end
def handle_call({tag, {:command_movement_calibrate, [axis]}} = code, _from, state) do
position = [x: 0.0, y: 0.0, z: 0.0]
state = %{state | position: position}
param_nr_steps = :"movement_axis_nr_steps_#{axis}"
param_nr_steps_val = Keyword.get(state.params, :param_nr_steps, 10_0000.00)
param_endpoints = :"movement_invert_endpoints_#{axis}"
param_endpoints_val = Keyword.get(state.params, :param_endpoints, 1.0)
resp_codes = [
GCODE.new(:report_echo, [GCODE.encode(code)]),
GCODE.new(:report_begin, [], tag),
GCODE.new(:report_busy, [], tag),
GCODE.new(:report_calibration_state, [:idle]),
GCODE.new(:report_calibration_state, [:home]),
GCODE.new(:report_calibration_state, [:end]),
GCODE.new(:report_calibration_paramater_value, [{param_nr_steps, param_nr_steps_val}]),
GCODE.new(:report_calibration_paramater_value, [{param_endpoints, param_endpoints_val}]),
GCODE.new(:report_position, state.position),
GCODE.new(:report_success, [], tag)
]
{:reply, :ok, state, {:continue, resp_codes}}
end
# Everything under this clause should be blocked if emergency_locked
def handle_call({_tag, {_, _}} = code, _from, %{status: :emergency_lock} = state) do
Logger.error("Stub Transport emergency lock")
resp_codes = [
GCODE.new(:report_echo, [GCODE.encode(code)]),
GCODE.new(:report_emergency_lock, [])
]
{:reply, :ok, state, {:continue, resp_codes}}
end
def handle_call({tag, {:pin_read, args}} = code, _from, state) do
p = Keyword.fetch!(args, :p)
m = Keyword.get(args, :m, state.pins[p][:m] || 0)
state =
case Map.get(state.pins, p) do
nil -> %{state | pins: Map.put(state.pins, p, m: m, v: 0)}
[m: ^m, v: v] -> %{state | pins: Map.put(state.pins, p, m: m, v: v)}
_ -> state
end
resp_codes = [
GCODE.new(:report_echo, [GCODE.encode(code)]),
GCODE.new(:report_begin, [], tag),
GCODE.new(:report_pin_value, p: p, v: Map.get(state.pins, p)[:v]),
GCODE.new(:report_success, [], tag)
]
{:reply, :ok, state, {:continue, resp_codes}}
end
def handle_call({tag, {:pin_write, args}} = code, _from, state) do
p = Keyword.fetch!(args, :p)
m = Keyword.get(args, :m, state.pins[p][:m] || 0)
v = Keyword.fetch!(args, :v)
state = %{state | pins: Map.put(state.pins, p, m: m, v: v)}
resp_codes = [
GCODE.new(:report_echo, [GCODE.encode(code)]),
GCODE.new(:report_begin, [], tag),
GCODE.new(:report_success, [], tag)
]
{:reply, :ok, state, {:continue, resp_codes}}
end
def handle_call({tag, {:position_read, _}} = code, _from, state) do
resp_codes = [
GCODE.new(:report_echo, [GCODE.encode(code)]),
GCODE.new(:report_begin, [], tag),
GCODE.new(:report_position, state.position),
GCODE.new(:report_success, [], tag)
]
{:reply, :ok, state, {:continue, resp_codes}}
end
def handle_call({tag, {:command_movement, args}} = code, _from, state) do
position = [
x: args[:x] || state.position[:x],
y: args[:y] || state.position[:y],
z: args[:z] || state.position[:z]
]
state = %{state | position: position}
resp_codes = [
GCODE.new(:report_echo, [GCODE.encode(code)]),
GCODE.new(:report_begin, [], tag),
GCODE.new(:report_busy, [], tag),
GCODE.new(:report_position, state.position),
GCODE.new(:report_success, [], tag)
]
{:reply, :ok, state, {:continue, resp_codes}}
end
def handle_call({tag, {:command_movement_home, [:x, :y, :z]}} = code, _from, state) do
position = [
x: 0.0,
y: 0.0,
z: 0.0
]
state = %{state | position: position}
resp_codes = [
GCODE.new(:report_echo, [GCODE.encode(code)]),
GCODE.new(:report_begin, [], tag),
GCODE.new(:report_busy, [], tag),
GCODE.new(:report_position, state.position),
GCODE.new(:report_success, [], tag)
]
{:reply, :ok, state, {:continue, resp_codes}}
end
def handle_call({tag, {:command_movement_home, [axis]}} = code, _from, state) do
position = Keyword.put(state.position, axis, 0.0) |> ensure_order()
state = %{state | position: position}
resp_codes = [
GCODE.new(:report_echo, [GCODE.encode(code)]),
GCODE.new(:report_begin, [], tag),
GCODE.new(:report_busy, [], tag),
GCODE.new(:report_position, state.position),
GCODE.new(:report_success, [], tag)
]
{:reply, :ok, state, {:continue, resp_codes}}
end
def handle_call({tag, {:command_movement_find_home, [axis]}} = code, _from, state) do
position = Keyword.put(state.position, axis, 0.0) |> ensure_order()
state = %{state | position: position}
resp_codes = [
GCODE.new(:report_echo, [GCODE.encode(code)]),
GCODE.new(:report_begin, [], tag),
GCODE.new(:report_busy, [], tag),
GCODE.new(:report_position, state.position),
GCODE.new(:report_success, [], tag)
]
{:reply, :ok, state, {:continue, resp_codes}}
end
def handle_call({tag, {_, _}} = code, _from, state) do
Logger.error("STUB HANDLER: unknown code: #{inspect(code)} for state: #{state.status}")
resp_codes = [
GCODE.new(:report_echo, [GCODE.encode(code)]),
GCODE.new(:report_invalid, [], tag)
]
{:reply, :ok, state, {:continue, resp_codes}}
end
def handle_continue([code | rest], state) do
state.handle_gcode.(code)
{:noreply, state, {:continue, rest}}
end
def handle_continue([], %{status: :idle} = state) do
{:noreply, state, 5_000}
end
def handle_continue([], %{status: _} = state) do
{:noreply, state}
end
defp goto(%{status: _old} = state, status), do: %{state | status: status}
defp ensure_order(pos) do
[
x: Keyword.fetch!(pos, :x),
y: Keyword.fetch!(pos, :y),
z: Keyword.fetch!(pos, :z)
]
end
end

View File

@ -0,0 +1,45 @@
defmodule Farmbot.Firmware.UARTTransport do
@moduledoc """
Handles sending/receiving GCODEs over UART.
This is the mechanism that official Farmbot's communicate with
official Farmbot-Arduino-Firmware's over.
"""
alias Farmbot.Firmware.GCODE
use GenServer
def init(args) do
device = Keyword.fetch!(args, :device)
handle_gcode = Keyword.fetch!(args, :handle_gcode)
{:ok, uart} = Nerves.UART.start_link()
{:ok, %{uart: uart, device: device, open: false, handle_gcode: handle_gcode}, 0}
end
def terminate(_, state) do
Nerves.UART.stop(state.uart)
end
def handle_info(:timeout, %{open: false} = state) do
opts = [active: true, speed: 115_200, framing: {Nerves.UART.Framing.Line, separator: "\r\n"}]
case Nerves.UART.open(state.uart, state.device, opts) do
:ok -> {:noreply, %{state | open: true}}
{:error, reason} -> {:stop, {:uart_error, reason}, state}
end
end
def handle_info({:nerves_uart, _, {:error, reason}}, state) do
{:stop, {:uart_error, reason}, state}
end
def handle_info({:nerves_uart, _, data}, state) when is_binary(data) do
code = GCODE.decode(String.trim(data))
state.handle_gcode.(code)
{:noreply, state}
end
def handle_call(code, _from, state) do
str = GCODE.encode(code)
r = Nerves.UART.write(state.uart, str)
{:reply, r, state}
end
end

View File

@ -0,0 +1,41 @@
defmodule Farmbot.Firmware.MixProject do
use Mix.Project
@version Path.join([__DIR__, "..", "VERSION"]) |> File.read!() |> String.trim()
@elixir_version Path.join([__DIR__, "..", "ELIXIR_VERSION"]) |> File.read!() |> String.trim()
def project do
[
app: :farmbot_firmware,
version: @version,
elixir: @elixir_version,
start_permanent: Mix.env() == :prod,
test_coverage: [tool: ExCoveralls],
preferred_cli_env: [
test: :test,
coveralls: :test,
"coveralls.circle": :test,
"coveralls.detail": :test,
"coveralls.post": :test,
"coveralls.html": :test
],
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:nerves_uart, "~> 1.2"},
{:excoveralls, "~> 0.10", only: [:test]},
{:dialyxir, "~> 1.0.0-rc.3", only: [:dev], runtime: false},
{:ex_doc, "~> 0.19", only: [:docs], runtime: false}
]
end
end

View File

@ -0,0 +1,42 @@
%{
"certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"},
"cowboy": {:hex, :cowboy, "2.5.0", "4ef3ae066ee10fe01ea3272edc8f024347a0d3eb95f6fbb9aed556dacbfc1337", [:rebar3], [{:cowlib, "~> 2.6.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.6.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
"cowlib": {:hex, :cowlib, "2.6.0", "8aa629f81a0fc189f261dc98a42243fa842625feea3c7ec56c48f4ccdb55490f", [:rebar3], [], "hexpm"},
"db_connection": {:hex, :db_connection, "1.1.3", "89b30ca1ef0a3b469b1c779579590688561d586694a3ce8792985d4d7e575a61", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
"decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], [], "hexpm"},
"dialyxir": {:hex, :dialyxir, "1.0.0-rc.4", "71b42f5ee1b7628f3e3a6565f4617dfb02d127a0499ab3e72750455e986df001", [:mix], [{:erlex, "~> 0.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"},
"earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"},
"ecto": {:hex, :ecto, "2.2.9", "031d55df9bb430cb118e6f3026a87408d9ce9638737bda3871e5d727a3594aae", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
"elixir_make": {:hex, :elixir_make, "0.4.2", "332c649d08c18bc1ecc73b1befc68c647136de4f340b548844efc796405743bf", [:mix], [], "hexpm"},
"erlex": {:hex, :erlex, "0.1.6", "c01c889363168d3fdd23f4211647d8a34c0f9a21ec726762312e08e083f3d47e", [:mix], [], "hexpm"},
"esqlite": {:hex, :esqlite, "0.2.4", "3a8a352c190afe2d6b828b252a6fbff65b5cc1124647f38b15bdab3bf6fd4b3e", [:rebar3], [], "hexpm"},
"ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
"excoveralls": {:hex, :excoveralls, "0.10.2", "fb4abd5b8a1b9d52d35e1162e7e2ea8bfb84b47ae07c38d39aa8ce64be0b0794", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
"gen_stage": {:hex, :gen_stage, "0.14.1", "9d46723fda072d4f4bb31a102560013f7960f5d80ea44dcb96fd6304ed61e7a4", [:mix], [], "hexpm"},
"gettext": {:hex, :gettext, "0.16.0", "4a7e90408cef5f1bf57c5a39e2db8c372a906031cc9b1466e963101cb927dafc", [:mix], [], "hexpm"},
"hackney": {:hex, :hackney, "1.14.3", "b5f6f5dcc4f1fba340762738759209e21914516df6be440d85772542d4a5e412", [:rebar3], [{:certifi, "2.4.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.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.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
"makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
"makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
"mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"},
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"},
"nerves_uart": {:hex, :nerves_uart, "1.2.0", "195424116b925cd3bf9d666be036c2a80655e6ca0f8d447e277667a60005c50e", [:mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"},
"nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"},
"plug": {:hex, :plug, "1.7.1", "8516d565fb84a6a8b2ca722e74e2cd25ca0fc9d64f364ec9dbec09d33eb78ccd", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}], "hexpm"},
"plug_cowboy": {:hex, :plug_cowboy, "2.0.0", "ab0c92728f2ba43c544cce85f0f220d8d30fc0c90eaa1e6203683ab039655062", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"},
"poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"},
"ranch": {:hex, :ranch, "1.6.2", "6db93c78f411ee033dbb18ba8234c5574883acb9a75af0fb90a9b82ea46afa00", [:rebar3], [], "hexpm"},
"sbroker": {:hex, :sbroker, "1.0.0", "28ff1b5e58887c5098539f236307b36fe1d3edaa2acff9d6a3d17c2dcafebbd0", [:rebar3], [], "hexpm"},
"sqlite_ecto2": {:hex, :sqlite_ecto2, "2.3.1", "fe58926854c3962c4c8710bd1070dd4ba3717ba77250387794cb7a65f77006aa", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "2.2.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: false]}, {:sqlitex, "~> 1.4", [hex: :sqlitex, repo: "hexpm", optional: false]}], "hexpm"},
"sqlitex": {:hex, :sqlitex, "1.4.3", "a50f12d6aeb25f4ebb128453386c09bbba8f5abd3c7713dc5eaa92f359926ac5", [:mix], [{:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:esqlite, "~> 0.2.4", [hex: :esqlite, repo: "hexpm", optional: false]}], "hexpm"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"},
"timex": {:hex, :timex, "3.4.2", "d74649c93ad0e12ce5b17cf5e11fbd1fb1b24a3d114643e86dba194b64439547", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
"tzdata": {:hex, :tzdata, "0.5.19", "7962a3997bf06303b7d1772988ede22260f3dae1bf897408ebdac2b4435f4e6a", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"},
}

View File

@ -0,0 +1,4 @@
defmodule Farmbot.FirmwareTest do
use ExUnit.Case
doctest Farmbot.Firmware
end

View File

@ -0,0 +1,315 @@
defmodule Farmbot.Firmware.GCODETest do
use ExUnit.Case
alias Farmbot.Firmware.GCODE
doctest GCODE
test "extracts q codes" do
assert {"0", ["ABC"]} = GCODE.extract_tag(["ABC", "Q0"])
assert {"123", ["Y00", "H1", "I1", "K9", "L199"]} =
GCODE.extract_tag(["Y00", "H1", "I1", "K9", "L199", "Q123"])
assert {"abc", ["J700"]} = GCODE.extract_tag(["J700", "Qabc"])
assert {nil, ["H100"]} = GCODE.extract_tag(["H100"])
end
describe "receive codes" do
test "idle" do
assert {nil, {:report_idle, []}} = GCODE.decode("R00")
assert {"100", {:report_idle, []}} = GCODE.decode("R00 Q100")
assert "R00" = GCODE.encode({nil, {:report_idle, []}})
assert "R00 Q100" = GCODE.encode({"100", {:report_idle, []}})
end
test "begin" do
assert {nil, {:report_begin, []}} = GCODE.decode("R01")
assert {"100", {:report_begin, []}} = GCODE.decode("R01 Q100")
assert "R01" = GCODE.encode({nil, {:report_begin, []}})
assert "R01 Q100" = GCODE.encode({"100", {:report_begin, []}})
end
test "success" do
assert {nil, {:report_success, []}} = GCODE.decode("R02")
assert {"100", {:report_success, []}} = GCODE.decode("R02 Q100")
assert "R02" = GCODE.encode({nil, {:report_success, []}})
assert "R02 Q100" = GCODE.encode({"100", {:report_success, []}})
end
test "error" do
assert {nil, {:report_error, []}} = GCODE.decode("R03")
assert {"100", {:report_error, []}} = GCODE.decode("R03 Q100")
assert "R03" = GCODE.encode({nil, {:report_error, []}})
assert "R03 Q100" = GCODE.encode({"100", {:report_error, []}})
end
test "busy" do
assert {nil, {:report_busy, []}} = GCODE.decode("R04")
assert {"100", {:report_busy, []}} = GCODE.decode("R04 Q100")
assert "R04" = GCODE.encode({nil, {:report_busy, []}})
assert "R04 Q100" = GCODE.encode({"100", {:report_busy, []}})
end
test "axis state" do
assert {nil, {:report_axis_state, [x: :idle]}} = GCODE.decode("R05 X0")
assert {nil, {:report_axis_state, [x: :begin]}} = GCODE.decode("R05 X1")
assert {nil, {:report_axis_state, [x: :accelerate]}} = GCODE.decode("R05 X2")
assert {nil, {:report_axis_state, [x: :cruise]}} = GCODE.decode("R05 X3")
assert {nil, {:report_axis_state, [x: :decelerate]}} = GCODE.decode("R05 X4")
assert {nil, {:report_axis_state, [x: :stop]}} = GCODE.decode("R05 X5")
assert {nil, {:report_axis_state, [x: :crawl]}} = GCODE.decode("R05 X6")
assert {"12", {:report_axis_state, [x: :idle]}} = GCODE.decode("R05 X0 Q12")
assert {"12", {:report_axis_state, [x: :begin]}} = GCODE.decode("R05 X1 Q12")
assert {"12", {:report_axis_state, [x: :accelerate]}} = GCODE.decode("R05 X2 Q12")
assert {"12", {:report_axis_state, [x: :cruise]}} = GCODE.decode("R05 X3 Q12")
assert {"12", {:report_axis_state, [x: :decelerate]}} = GCODE.decode("R05 X4 Q12")
assert {"12", {:report_axis_state, [x: :stop]}} = GCODE.decode("R05 X5 Q12")
assert {"12", {:report_axis_state, [x: :crawl]}} = GCODE.decode("R05 X6 Q12")
assert "R05 X0" = GCODE.encode({nil, {:report_axis_state, [x: :idle]}})
assert "R05 X1" = GCODE.encode({nil, {:report_axis_state, [x: :begin]}})
assert "R05 X2" = GCODE.encode({nil, {:report_axis_state, [x: :accelerate]}})
assert "R05 X3" = GCODE.encode({nil, {:report_axis_state, [x: :cruise]}})
assert "R05 X4" = GCODE.encode({nil, {:report_axis_state, [x: :decelerate]}})
assert "R05 X5" = GCODE.encode({nil, {:report_axis_state, [x: :stop]}})
assert "R05 X6" = GCODE.encode({nil, {:report_axis_state, [x: :crawl]}})
assert "R05 X0 Q12" = GCODE.encode({"12", {:report_axis_state, [x: :idle]}})
assert "R05 X1 Q12" = GCODE.encode({"12", {:report_axis_state, [x: :begin]}})
assert "R05 X2 Q12" = GCODE.encode({"12", {:report_axis_state, [x: :accelerate]}})
assert "R05 X3 Q12" = GCODE.encode({"12", {:report_axis_state, [x: :cruise]}})
assert "R05 X4 Q12" = GCODE.encode({"12", {:report_axis_state, [x: :decelerate]}})
assert "R05 X5 Q12" = GCODE.encode({"12", {:report_axis_state, [x: :stop]}})
assert "R05 X6 Q12" = GCODE.encode({"12", {:report_axis_state, [x: :crawl]}})
assert {nil, {:report_axis_state, [y: :idle]}} = GCODE.decode("R05 Y0")
assert {nil, {:report_axis_state, [y: :begin]}} = GCODE.decode("R05 Y1")
assert {nil, {:report_axis_state, [y: :accelerate]}} = GCODE.decode("R05 Y2")
assert {nil, {:report_axis_state, [y: :cruise]}} = GCODE.decode("R05 Y3")
assert {nil, {:report_axis_state, [y: :decelerate]}} = GCODE.decode("R05 Y4")
assert {nil, {:report_axis_state, [y: :stop]}} = GCODE.decode("R05 Y5")
assert {nil, {:report_axis_state, [y: :crawl]}} = GCODE.decode("R05 Y6")
assert {"13", {:report_axis_state, [y: :idle]}} = GCODE.decode("R05 Y0 Q13")
assert {"13", {:report_axis_state, [y: :begin]}} = GCODE.decode("R05 Y1 Q13")
assert {"13", {:report_axis_state, [y: :accelerate]}} = GCODE.decode("R05 Y2 Q13")
assert {"13", {:report_axis_state, [y: :cruise]}} = GCODE.decode("R05 Y3 Q13")
assert {"13", {:report_axis_state, [y: :decelerate]}} = GCODE.decode("R05 Y4 Q13")
assert {"13", {:report_axis_state, [y: :stop]}} = GCODE.decode("R05 Y5 Q13")
assert {"13", {:report_axis_state, [y: :crawl]}} = GCODE.decode("R05 Y6 Q13")
assert "R05 Y0" = GCODE.encode({nil, {:report_axis_state, [y: :idle]}})
assert "R05 Y1" = GCODE.encode({nil, {:report_axis_state, [y: :begin]}})
assert "R05 Y2" = GCODE.encode({nil, {:report_axis_state, [y: :accelerate]}})
assert "R05 Y3" = GCODE.encode({nil, {:report_axis_state, [y: :cruise]}})
assert "R05 Y4" = GCODE.encode({nil, {:report_axis_state, [y: :decelerate]}})
assert "R05 Y5" = GCODE.encode({nil, {:report_axis_state, [y: :stop]}})
assert "R05 Y6" = GCODE.encode({nil, {:report_axis_state, [y: :crawl]}})
assert "R05 Y0 Q13" = GCODE.encode({"13", {:report_axis_state, [y: :idle]}})
assert "R05 Y1 Q13" = GCODE.encode({"13", {:report_axis_state, [y: :begin]}})
assert "R05 Y2 Q13" = GCODE.encode({"13", {:report_axis_state, [y: :accelerate]}})
assert "R05 Y3 Q13" = GCODE.encode({"13", {:report_axis_state, [y: :cruise]}})
assert "R05 Y4 Q13" = GCODE.encode({"13", {:report_axis_state, [y: :decelerate]}})
assert "R05 Y5 Q13" = GCODE.encode({"13", {:report_axis_state, [y: :stop]}})
assert "R05 Y6 Q13" = GCODE.encode({"13", {:report_axis_state, [y: :crawl]}})
assert {nil, {:report_axis_state, [z: :idle]}} = GCODE.decode("R05 Z0")
assert {nil, {:report_axis_state, [z: :begin]}} = GCODE.decode("R05 Z1")
assert {nil, {:report_axis_state, [z: :accelerate]}} = GCODE.decode("R05 Z2")
assert {nil, {:report_axis_state, [z: :cruise]}} = GCODE.decode("R05 Z3")
assert {nil, {:report_axis_state, [z: :decelerate]}} = GCODE.decode("R05 Z4")
assert {nil, {:report_axis_state, [z: :stop]}} = GCODE.decode("R05 Z5")
assert {nil, {:report_axis_state, [z: :crawl]}} = GCODE.decode("R05 Z6")
assert {"14", {:report_axis_state, [z: :idle]}} = GCODE.decode("R05 Z0 Q14")
assert {"14", {:report_axis_state, [z: :begin]}} = GCODE.decode("R05 Z1 Q14")
assert {"14", {:report_axis_state, [z: :accelerate]}} = GCODE.decode("R05 Z2 Q14")
assert {"14", {:report_axis_state, [z: :cruise]}} = GCODE.decode("R05 Z3 Q14")
assert {"14", {:report_axis_state, [z: :decelerate]}} = GCODE.decode("R05 Z4 Q14")
assert {"14", {:report_axis_state, [z: :stop]}} = GCODE.decode("R05 Z5 Q14")
assert {"14", {:report_axis_state, [z: :crawl]}} = GCODE.decode("R05 Z6 Q14")
assert "R05 Z0" = GCODE.encode({nil, {:report_axis_state, [z: :idle]}})
assert "R05 Z1" = GCODE.encode({nil, {:report_axis_state, [z: :begin]}})
assert "R05 Z2" = GCODE.encode({nil, {:report_axis_state, [z: :accelerate]}})
assert "R05 Z3" = GCODE.encode({nil, {:report_axis_state, [z: :cruise]}})
assert "R05 Z4" = GCODE.encode({nil, {:report_axis_state, [z: :decelerate]}})
assert "R05 Z5" = GCODE.encode({nil, {:report_axis_state, [z: :stop]}})
assert "R05 Z6" = GCODE.encode({nil, {:report_axis_state, [z: :crawl]}})
assert "R05 Z0 Q14" = GCODE.encode({"14", {:report_axis_state, [z: :idle]}})
assert "R05 Z1 Q14" = GCODE.encode({"14", {:report_axis_state, [z: :begin]}})
assert "R05 Z2 Q14" = GCODE.encode({"14", {:report_axis_state, [z: :accelerate]}})
assert "R05 Z3 Q14" = GCODE.encode({"14", {:report_axis_state, [z: :cruise]}})
assert "R05 Z4 Q14" = GCODE.encode({"14", {:report_axis_state, [z: :decelerate]}})
assert "R05 Z5 Q14" = GCODE.encode({"14", {:report_axis_state, [z: :stop]}})
assert "R05 Z6 Q14" = GCODE.encode({"14", {:report_axis_state, [z: :crawl]}})
end
test "calibration" do
assert {nil, {:report_calibration_state, [x: :idle]}} = GCODE.decode("R06 X0")
assert {nil, {:report_calibration_state, [x: :home]}} = GCODE.decode("R06 X1")
assert {nil, {:report_calibration_state, [x: :end]}} = GCODE.decode("R06 X2")
assert {"1", {:report_calibration_state, [x: :idle]}} = GCODE.decode("R06 X0 Q1")
assert {"1", {:report_calibration_state, [x: :home]}} = GCODE.decode("R06 X1 Q1")
assert {"1", {:report_calibration_state, [x: :end]}} = GCODE.decode("R06 X2 Q1")
assert "R06 X0" = GCODE.encode({nil, {:report_calibration_state, [x: :idle]}})
assert "R06 X1" = GCODE.encode({nil, {:report_calibration_state, [x: :home]}})
assert "R06 X2" = GCODE.encode({nil, {:report_calibration_state, [x: :end]}})
assert "R06 X0 Q1" = GCODE.encode({"1", {:report_calibration_state, [x: :idle]}})
assert "R06 X1 Q1" = GCODE.encode({"1", {:report_calibration_state, [x: :home]}})
assert "R06 X2 Q1" = GCODE.encode({"1", {:report_calibration_state, [x: :end]}})
assert {nil, {:report_calibration_state, [y: :idle]}} = GCODE.decode("R06 Y0")
assert {nil, {:report_calibration_state, [y: :home]}} = GCODE.decode("R06 Y1")
assert {nil, {:report_calibration_state, [y: :end]}} = GCODE.decode("R06 Y2")
assert {"1", {:report_calibration_state, [y: :idle]}} = GCODE.decode("R06 Y0 Q1")
assert {"1", {:report_calibration_state, [y: :home]}} = GCODE.decode("R06 Y1 Q1")
assert {"1", {:report_calibration_state, [y: :end]}} = GCODE.decode("R06 Y2 Q1")
assert "R06 Y0" = GCODE.encode({nil, {:report_calibration_state, [y: :idle]}})
assert "R06 Y1" = GCODE.encode({nil, {:report_calibration_state, [y: :home]}})
assert "R06 Y2" = GCODE.encode({nil, {:report_calibration_state, [y: :end]}})
assert "R06 Y0 Q1" = GCODE.encode({"1", {:report_calibration_state, [y: :idle]}})
assert "R06 Y1 Q1" = GCODE.encode({"1", {:report_calibration_state, [y: :home]}})
assert "R06 Y2 Q1" = GCODE.encode({"1", {:report_calibration_state, [y: :end]}})
assert {nil, {:report_calibration_state, [z: :idle]}} = GCODE.decode("R06 Z0")
assert {nil, {:report_calibration_state, [z: :home]}} = GCODE.decode("R06 Z1")
assert {nil, {:report_calibration_state, [z: :end]}} = GCODE.decode("R06 Z2")
assert {"1", {:report_calibration_state, [z: :idle]}} = GCODE.decode("R06 Z0 Q1")
assert {"1", {:report_calibration_state, [z: :home]}} = GCODE.decode("R06 Z1 Q1")
assert {"1", {:report_calibration_state, [z: :end]}} = GCODE.decode("R06 Z2 Q1")
assert "R06 Z0" = GCODE.encode({nil, {:report_calibration_state, [z: :idle]}})
assert "R06 Z1" = GCODE.encode({nil, {:report_calibration_state, [z: :home]}})
assert "R06 Z2" = GCODE.encode({nil, {:report_calibration_state, [z: :end]}})
assert "R06 Z0 Q1" = GCODE.encode({"1", {:report_calibration_state, [z: :idle]}})
assert "R06 Z1 Q1" = GCODE.encode({"1", {:report_calibration_state, [z: :home]}})
assert "R06 Z2 Q1" = GCODE.encode({"1", {:report_calibration_state, [z: :end]}})
end
test "retry" do
assert {nil, {:report_retry, []}} = GCODE.decode("R07")
assert {"100", {:report_retry, []}} = GCODE.decode("R07 Q100")
assert "R07" = GCODE.encode({nil, {:report_retry, []}})
assert "R07 Q100" = GCODE.encode({"100", {:report_retry, []}})
end
test "echo" do
assert {nil, {:report_echo, ["ABC"]}} = GCODE.decode("R08 * ABC *")
assert "R08 * ABC *" = GCODE.encode({nil, {:report_echo, ["ABC"]}})
end
test "invalid" do
assert {nil, {:report_invalid, []}} = GCODE.decode("R09")
assert {"50", {:report_invalid, []}} = GCODE.decode("R09 Q50")
assert "R09" = GCODE.encode({nil, {:report_invalid, []}})
assert "R09 Q50" = GCODE.encode({"50", {:report_invalid, []}})
end
test "home complete" do
assert {nil, {:report_home_complete, [:x]}} = GCODE.decode("R11")
assert {"22", {:report_home_complete, [:x]}} = GCODE.decode("R11 Q22")
assert {nil, {:report_home_complete, [:y]}} = GCODE.decode("R12")
assert {"22", {:report_home_complete, [:y]}} = GCODE.decode("R12 Q22")
assert {nil, {:report_home_complete, [:z]}} = GCODE.decode("R13")
assert {"22", {:report_home_complete, [:z]}} = GCODE.decode("R13 Q22")
end
test "position change" do
assert {nil, {:report_position_change, [{:x, 200.0}]}} = GCODE.decode("R15 X200")
assert {"33", {:report_position_change, [{:x, 200.0}]}} = GCODE.decode("R15 X200 Q33")
assert {nil, {:report_position_change, [{:y, 200.0}]}} = GCODE.decode("R16 Y200")
assert {"33", {:report_position_change, [{:y, 200.0}]}} = GCODE.decode("R17 Y200 Q33")
assert {nil, {:report_position_change, [{:z, 200.0}]}} = GCODE.decode("R15 Z200")
assert {"33", {:report_position_change, [{:z, 200.0}]}} = GCODE.decode("R15 Z200 Q33")
end
test "paramater report complete" do
assert {nil, {:report_paramaters_complete, []}} = GCODE.decode("R20")
assert {"66", {:report_paramaters_complete, []}} = GCODE.decode("R20 Q66")
end
test "axis timeout" do
assert {nil, {:report_axis_timeout, [:x]}} = GCODE.decode("R71")
assert {"22", {:report_axis_timeout, [:x]}} = GCODE.decode("R71 Q22")
assert {nil, {:report_axis_timeout, [:y]}} = GCODE.decode("R72")
assert {"22", {:report_axis_timeout, [:y]}} = GCODE.decode("R72 Q22")
assert {nil, {:report_axis_timeout, [:z]}} = GCODE.decode("R73")
assert {"22", {:report_axis_timeout, [:z]}} = GCODE.decode("R73 Q22")
end
test "end stops" do
assert {nil, {:report_end_stops, [xa: 1, xb: 0, ya: 0, yb: 1, za: 1, zb: 0]}} =
GCODE.decode("R81 XA1 XB0 YA0 YB1 ZA1 ZB0")
end
test "position" do
assert {nil, {:report_position, [{:x, 100.0}, {:y, 200.0}, {:z, 400.0}]}} =
GCODE.decode("R82 X100 Y200 Z400")
assert {"1", {:report_position, [{:x, 100.0}, {:y, 200.0}, {:z, 400.0}]}} =
GCODE.decode("R82 X100 Y200 Z400 Q1")
assert {nil, {:report_position, [{:x, 100.0}, {:z, 12.0}]}} = GCODE.decode("R82 X100 Z12")
assert {nil, {:report_position, [{:z, 5.0}]}} = GCODE.decode("R82 Z5")
end
test "version" do
assert {nil, {:report_software_version, ["6.5.0.G"]}} = GCODE.decode("R83 6.5.0.G")
assert {"900", {:report_software_version, ["6.5.0.G"]}} = GCODE.decode("R83 6.5.0.G Q900")
assert "R83 6.5.0.G" = GCODE.encode({nil, {:report_software_version, ["6.5.0.G"]}})
assert "R83 6.5.0.G Q900" = GCODE.encode({"900", {:report_software_version, ["6.5.0.G"]}})
end
test "encoders" do
assert {nil, {:report_encoders_scaled, [{:x, 100.0}, {:y, 200.0}, {:z, 400.0}]}} =
GCODE.decode("R84 X100 Y200 Z400")
assert {"1", {:report_encoders_scaled, [{:x, 100.0}, {:y, 200.0}, {:z, 400.0}]}} =
GCODE.decode("R84 X100 Y200 Z400 Q1")
assert {nil, {:report_encoders_raw, [{:x, 100.0}, {:y, 200.0}, {:z, 400.0}]}} =
GCODE.decode("R85 X100 Y200 Z400")
assert {"1", {:report_encoders_raw, [{:x, 100.0}, {:y, 200.0}, {:z, 400.0}]}} =
GCODE.decode("R85 X100 Y200 Z400 Q1")
end
test "emergency lock" do
assert {nil, {:report_emergency_lock, []}} = GCODE.decode("R87")
assert {"999", {:report_emergency_lock, []}} = GCODE.decode("R87 Q999")
assert "R87" = GCODE.encode({nil, {:report_emergency_lock, []}})
assert "R87 Q999" = GCODE.encode({"999", {:report_emergency_lock, []}})
end
test "debug message" do
assert {nil, {:report_debug_message, ["Hello, World!"]}} = GCODE.decode("R99 Hello, World!")
assert "R99 Hello, World!" = GCODE.encode({nil, {:report_debug_message, ["Hello, World!"]}})
end
end
end

View File

@ -0,0 +1 @@
ExUnit.start()

View File

@ -1,5 +1,10 @@
[
import_deps: [:ecto],
inputs: ["*.{ex,exs}", "{config,priv,test}/**/*.{ex,exs}"],
inputs: [
"*.{ex,exs}",
"{config,priv,test}/**/*.{ex,exs}",
"lib/celery_script/**/*.{ex,exs}",
"platform/**/*.{ex,exs}"
],
subdirectories: ["priv/*/migrations"]
]

Binary file not shown.

View File

@ -1,28 +1,14 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.
use Mix.Config
# Mix configs.
target = Mix.Project.config()[:target]
env = Mix.env()
config :farmbot_core, Farmbot.AssetWorker.Farmbot.Asset.FarmEvent, checkup_time_ms: 10_000
config :logger, [
utc_log: true,
handle_otp_reports: true,
handle_sasl_reports: true,
backends: [:console]
]
config :farmbot_core, Farmbot.AssetWorker.Farmbot.Asset.FarmwareInstallation,
error_retry_time_ms: 30_000,
install_dir: "/tmp/farmware"
# Randomly picked 300 megabytes.
# 3964928 bytes == ~4 megabytes in sqlite3
# 9266 logs = ~4 megabytes
# 4 logs * 75 = 300 megabytes
# 9266 logs * 75 = 694950 logs
# This will trim 175000 logs (25%) every time it gets to the max logs.
config :logger_backend_ecto, max_logs: 700000
config :farmbot_core, Elixir.Farmbot.AssetWorker.Farmbot.Asset.PinBinding,
gpio_handler: Farmbot.PinBindingWorker.StubGPIOHandler,
error_retry_time_ms: 30_000
# Customize non-Elixir parts of the firmware. See
# https://hexdocs.pm/nerves/advanced-configuration.html for details.
@ -30,74 +16,51 @@ config :nerves, :firmware,
rootfs_overlay: "rootfs_overlay",
provisioning: :nerves_hub
# Use shoehorn to start the main application. See the shoehorn
# docs for separating out critical OTP applications such as those
# involved with firmware updates.
config :shoehorn,
init: [:nerves_runtime, :nerves_init_gadget],
handler: Farmbot.OS.ShoehornHandler,
app: Mix.Project.config()[:app]
# Stop lager redirecting :error_logger messages
config :lager, :error_logger_redirect, false
# Stop lager removing Logger's :error_logger handler
config :lager, :error_logger_whitelist, []
# Stop lager writing a crash log
config :lager, :crash_log, false
# Use LagerLogger as lager's only handler.
config :lager, :handlers, []
config :ssl, protocol_version: :"tlsv1.2"
# Disable tzdata autoupdates because it tries to dl the update file
# Before we have network or ntp.
config :tzdata, :autoupdate, :disabled
config :tesla, Tesla.Middleware.Logger,
format: "$method $url ====> $status / time=$time",
debug: false
config :farmbot_core, :behaviour,
firmware_handler: Farmbot.Firmware.StubHandler,
leds_handler: Farmbot.Leds.StubHandler,
pin_binding_handler: Farmbot.PinBinding.StubHandler,
celery_script_io_layer: Farmbot.OS.IOLayer,
json_parser: Farmbot.JSON.JasonParser
config :farmbot_core, Farmbot.AssetMonitor, checkup_time_ms: 30_000
config :farmbot_core,
expected_fw_versions: ["6.4.0.F", "6.4.0.R", "6.4.0.G"],
default_firmware_io_logs: false,
default_server: "https://my.farm.bot",
default_currently_on_beta: String.contains?(to_string(:os.cmd('git rev-parse --abbrev-ref HEAD')), "beta"),
firmware_io_logs: false,
farm_event_debug_log: false
default_currently_on_beta:
String.contains?(to_string(:os.cmd('git rev-parse --abbrev-ref HEAD')), "beta")
config :farmbot_ext, :behaviour,
authorization: Farmbot.Bootstrap.Authorization
# Configure Farmbot Behaviours.
config :farmbot_core, :behaviour,
leds_handler: Farmbot.Leds.StubHandler,
celery_script_io_layer: Farmbot.OS.IOLayer,
json_parser: Farmbot.JSON.JasonParser
config :farmbot_ext, :behaviour, authorization: Farmbot.Bootstrap.Authorization
config :ecto, json_library: Farmbot.JSON
config :farmbot_os,
ecto_repos: [Farmbot.Config.Repo, Farmbot.Logger.Repo, Farmbot.Asset.Repo]
config :farmbot_os, :builtins,
sequence: [
emergency_lock: -1,
emergency_unlock: -2,
sync: -3,
reboot: -4,
power_off: -5
],
pin_binding: [
emergency_lock: -1,
emergency_unlock: -2,
config :farmbot_core, Farmbot.Config.Repo,
adapter: Sqlite.Ecto2,
loggers: [],
database: "config.#{Mix.env()}.db",
priv: "../farmbot_core/priv/config"
config :farmbot_core, Farmbot.Logger.Repo,
adapter: Sqlite.Ecto2,
loggers: [],
database: "logger.#{Mix.env()}.db",
priv: "../farmbot_core/priv/logger"
config :farmbot_core, Farmbot.Asset.Repo,
adapter: Sqlite.Ecto2,
loggers: [],
database: "asset.#{Mix.env()}.db",
priv: "../farmbot_core/priv/asset"
config :farmbot_os, Farmbot.OS.FileSystem, data_path: "/tmp/farmbot"
config :farmbot_os, Farmbot.System, system_tasks: Farmbot.Host.SystemTasks
config :farmbot_os, Farmbot.Platform.Supervisor,
platform_children: [
Farmbot.Host.Configurator
]
case target do
"host" ->
import_config("host/#{env}.exs")
_ ->
import_config("target/#{env}.exs")
if File.exists?("config/target/#{target}.exs"),
do: import_config("target/#{target}.exs")
end
import_config("lagger.exs")

Some files were not shown because too many files have changed in this diff Show More