Merge pull request #1202 from FarmBot/prod_release

Prod Release, v10.0.0
master
Rick Carlino 2020-05-19 09:56:53 -05:00 committed by GitHub
commit c2b92722a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 7189 additions and 6298 deletions

View File

@ -2,7 +2,7 @@ version: 2.0
defaults: &defaults defaults: &defaults
working_directory: /nerves/build working_directory: /nerves/build
docker: docker:
- image: nervesproject/nerves_system_br:latest - image: nervesproject/nerves_system_br:1.11.3
install_elixir: &install_elixir install_elixir: &install_elixir
run: run:

View File

@ -1,5 +1,12 @@
# Changelog # Changelog
# 10.0.0
* Deprecate `resource_update` RPC
* Introduce `update_resource` RPC, which allows users to modify variables from the sequence editor.
* Genesis v1.5 and Express v1.0 firmware updates.
* Fix a bug where FBOS would not honor an "AUTO UPDATE" value of "false".
# 9.2.2 # 9.2.2
* Fix firmware locking error ("Can't perform X in Y state") * Fix firmware locking error ("Can't perform X in Y state")

View File

@ -5,6 +5,7 @@
"assertion_block": "8.0.0", "assertion_block": "8.0.0",
"backscheduled_regimens": "6.4.0", "backscheduled_regimens": "6.4.0",
"change_ownership": "6.3.0", "change_ownership": "6.3.0",
"criteria_groups": "9.2.2",
"diagnostic_dumps": "6.4.4", "diagnostic_dumps": "6.4.4",
"endstop_invert": "6.4.1", "endstop_invert": "6.4.1",
"express_k10": "8.0.0", "express_k10": "8.0.0",
@ -20,6 +21,7 @@
"ota_update_hour": "8.2.3", "ota_update_hour": "8.2.3",
"rpi_led_control": "6.4.4", "rpi_led_control": "6.4.4",
"sensors": "6.3.0", "sensors": "6.3.0",
"update_resource": "10.0.0",
"use_update_channel": "6.4.12", "use_update_channel": "6.4.12",
"variables": "8.0.0" "variables": "8.0.0"
} }

View File

@ -28,3 +28,14 @@ This release uses an improved Farmware API:
# v9 # v9
FarmBot OS v8+ uses an improved Farmware API. See the [Farmware developer documentation](https://developer.farm.bot/docs/farmware) for more information. FarmBot OS v8+ uses an improved Farmware API. See the [Farmware developer documentation](https://developer.farm.bot/docs/farmware) for more information.
# v10
FarmBot OS v10 features an improved *Mark As* step. If you have previously added *Mark As* steps to sequences, you will need to update them before they can be executed by FarmBot:
* Open any sequences with a caution icon next to the name.
* Click the `CONVERT` button in each old *Mark As* step.
* Save the sequence.
* If you have auto-sync disabled, press `SYNC NOW` once all sequences have been updated.
* Verify that any events using the updated sequences are running as expected.
FarmBot OS auto-update was disabled prior to this release.

View File

@ -1 +1 @@
9.2.2 10.0.0

View File

@ -22,13 +22,14 @@
{ {
"name": "ALLOWED_MESSAGE_TYPES", "name": "ALLOWED_MESSAGE_TYPES",
"allowed_values": [ "allowed_values": [
"success", "assertion",
"busy", "busy",
"warn", "debug",
"error", "error",
"info",
"fun", "fun",
"debug" "info",
"success",
"warn"
] ]
}, },
{ {
@ -55,6 +56,15 @@
1 1
] ]
}, },
{
"name": "ALLOWED_ASSERTION_TYPES",
"allowed_values": [
"abort",
"recover",
"abort_recover",
"continue"
]
},
{ {
"name": "AllowedPinTypes", "name": "AllowedPinTypes",
"allowed_values": [ "allowed_values": [
@ -89,6 +99,7 @@
"name": "LegalSequenceKind", "name": "LegalSequenceKind",
"allowed_values": [ "allowed_values": [
"_if", "_if",
"assertion",
"calibrate", "calibrate",
"change_ownership", "change_ownership",
"check_updates", "check_updates",
@ -109,7 +120,6 @@
"read_status", "read_status",
"reboot", "reboot",
"remove_farmware", "remove_farmware",
"resource_update",
"send_message", "send_message",
"set_servo_angle", "set_servo_angle",
"set_user_env", "set_user_env",
@ -117,6 +127,7 @@
"take_photo", "take_photo",
"toggle_pin", "toggle_pin",
"update_farmware", "update_farmware",
"update_resource",
"wait", "wait",
"write_pin", "write_pin",
"zero" "zero"
@ -206,7 +217,8 @@
"planned", "planned",
"planted", "planted",
"harvested", "harvested",
"sprouted" "sprouted",
"removed"
] ]
}, },
{ {
@ -214,24 +226,18 @@
"allowed_values": [ "allowed_values": [
"GenericPointer", "GenericPointer",
"ToolSlot", "ToolSlot",
"Plant" "Plant",
"Weed"
] ]
}, },
{ {
"name": "resource_type", "name": "resource_type",
"allowed_values": [ "allowed_values": [
"Device", "Device",
"FarmEvent",
"Image",
"Log",
"Peripheral",
"Plant",
"Point", "Point",
"Regimen", "Plant",
"Sequence",
"Tool",
"ToolSlot", "ToolSlot",
"User", "Weed",
"GenericPointer" "GenericPointer"
] ]
}, },
@ -261,11 +267,13 @@
"z", "z",
"pin_type", "pin_type",
"pointer_id", "pointer_id",
"point_group_id",
"pointer_type", "pointer_type",
"pin_mode", "pin_mode",
"sequence_id", "sequence_id",
"lhs", "lhs",
"op", "op",
"priority",
"channel_name", "channel_name",
"message_type", "message_type",
"tool_id", "tool_id",
@ -273,19 +281,22 @@
"axis", "axis",
"message", "message",
"speed", "speed",
"resource_type" "resource_type",
"assertion_type",
"lua",
"resource"
] ]
}, },
{ {
"name": "LegalKindString", "name": "LegalKindString",
"allowed_values": [ "allowed_values": [
"Assertion",
"If", "If",
"Calibrate", "Calibrate",
"ChangeOwnership", "ChangeOwnership",
"Channel", "Channel",
"CheckUpdates", "CheckUpdates",
"Coordinate", "Coordinate",
"DumpInfo",
"EmergencyLock", "EmergencyLock",
"EmergencyUnlock", "EmergencyUnlock",
"ExecuteScript", "ExecuteScript",
@ -331,7 +342,10 @@
"MoveAbsolute", "MoveAbsolute",
"WritePin", "WritePin",
"ReadPin", "ReadPin",
"ResourceUpdate" "ResourceUpdate",
"Resource",
"UpdateResource",
"PointGroup"
] ]
} }
], ],
@ -394,6 +408,10 @@
{ {
"tag": "identifier", "tag": "identifier",
"name": "identifier" "name": "identifier"
},
{
"tag": "point_group",
"name": "point_group"
} }
] ]
}, },
@ -625,6 +643,15 @@
} }
] ]
}, },
{
"name": "point_group_id",
"allowed_values": [
{
"tag": "integer",
"name": "Integer"
}
]
},
{ {
"name": "pointer_type", "name": "pointer_type",
"allowed_values": [ "allowed_values": [
@ -674,6 +701,15 @@
} }
] ]
}, },
{
"name": "priority",
"allowed_values": [
{
"tag": "integer",
"name": "Integer"
}
]
},
{ {
"name": "channel_name", "name": "channel_name",
"allowed_values": [ "allowed_values": [
@ -745,9 +781,53 @@
"name": "resource_type" "name": "resource_type"
} }
] ]
},
{
"name": "assertion_type",
"allowed_values": [
{
"tag": "ALLOWED_ASSERTION_TYPES",
"name": "ALLOWED_ASSERTION_TYPES"
}
]
},
{
"name": "lua",
"allowed_values": [
{
"tag": "string",
"name": "String"
}
]
},
{
"name": "resource",
"allowed_values": [
{
"tag": "identifier",
"name": "identifier"
},
{
"tag": "resource",
"name": "resource"
}
]
} }
], ],
"nodes": [ "nodes": [
{
"allowed_args": [
"assertion_type",
"_then",
"lua"
],
"allowed_body_types": [],
"name": "assertion",
"tags": [
"*"
],
"docs": ""
},
{ {
"allowed_args": [ "allowed_args": [
"lhs", "lhs",
@ -1151,9 +1231,11 @@
}, },
{ {
"allowed_args": [ "allowed_args": [
"label" "label",
"priority"
], ],
"allowed_body_types": [ "allowed_body_types": [
"assertion",
"calibrate", "calibrate",
"change_ownership", "change_ownership",
"check_updates", "check_updates",
@ -1175,7 +1257,7 @@
"read_status", "read_status",
"reboot", "reboot",
"remove_farmware", "remove_farmware",
"resource_update", "update_resource",
"send_message", "send_message",
"set_servo_angle", "set_servo_angle",
"set_user_env", "set_user_env",
@ -1225,6 +1307,7 @@
"locals" "locals"
], ],
"allowed_body_types": [ "allowed_body_types": [
"assertion",
"calibrate", "calibrate",
"change_ownership", "change_ownership",
"check_updates", "check_updates",
@ -1246,7 +1329,7 @@
"read_status", "read_status",
"reboot", "reboot",
"remove_farmware", "remove_farmware",
"resource_update", "update_resource",
"send_message", "send_message",
"set_servo_angle", "set_servo_angle",
"set_user_env", "set_user_env",
@ -1459,6 +1542,45 @@
"network_user" "network_user"
], ],
"docs": "" "docs": ""
},
{
"allowed_args": [
"resource_type",
"resource_id"
],
"allowed_body_types": [],
"name": "resource",
"tags": [
"network_user"
],
"docs": ""
},
{
"allowed_args": [
"resource"
],
"allowed_body_types": [
"pair"
],
"name": "update_resource",
"tags": [
"function",
"api_writer",
"network_user"
],
"docs": ""
},
{
"allowed_args": [
"point_group_id"
],
"allowed_body_types": [],
"name": "point_group",
"tags": [
"data",
"list_like"
],
"docs": ""
} }
] ]
} }

View File

@ -11,17 +11,10 @@ defmodule FarmbotCeleryScript.Compiler do
Compiler.IdentifierSanitizer Compiler.IdentifierSanitizer
} }
@doc "Sets debug mode for the compiler"
def debug_mode(bool \\ true) do
old = Application.get_env(:farmbot_celery_script, __MODULE__, [])
new = Keyword.put(old, :debug, bool)
Application.put_env(:farmbot_celery_script, __MODULE__, new)
bool
end
@doc "Returns current debug mode value" @doc "Returns current debug mode value"
def debug_mode?() do def debug_mode?() do
Application.get_env(:farmbot_celery_script, __MODULE__)[:debug] || false # Set this to `true` when debuging.
false
end end
@valid_entry_points [:sequence, :rpc_request] @valid_entry_points [:sequence, :rpc_request]
@ -94,27 +87,28 @@ defmodule FarmbotCeleryScript.Compiler do
defdelegate assertion(ast, env), to: Compiler.Assertion defdelegate assertion(ast, env), to: Compiler.Assertion
defdelegate calibrate(ast, env), to: Compiler.AxisControl defdelegate calibrate(ast, env), to: Compiler.AxisControl
defdelegate coordinate(ast, env), to: Compiler.DataControl defdelegate coordinate(ast, env), to: Compiler.DataControl
defdelegate execute(ast, env), to: Compiler.Execute
defdelegate execute_script(ast, env), to: Compiler.Farmware defdelegate execute_script(ast, env), to: Compiler.Farmware
defdelegate execute(ast, env), to: Compiler.Execute
defdelegate find_home(ast, env), to: Compiler.AxisControl defdelegate find_home(ast, env), to: Compiler.AxisControl
defdelegate home(ast, env), to: Compiler.AxisControl defdelegate home(ast, env), to: Compiler.AxisControl
defdelegate unquote(:_if)(ast, env), to: Compiler.If
defdelegate install_first_party_farmware(ast, env), to: Compiler.Farmware defdelegate install_first_party_farmware(ast, env), to: Compiler.Farmware
defdelegate move_absolute(ast, env), to: Compiler.AxisControl defdelegate move_absolute(ast, env), to: Compiler.AxisControl
defdelegate move_relative(ast, env), to: Compiler.AxisControl defdelegate move_relative(ast, env), to: Compiler.AxisControl
defdelegate named_pin(ast, env), to: Compiler.DataControl defdelegate named_pin(ast, env), to: Compiler.DataControl
defdelegate point(ast, env), to: Compiler.DataControl defdelegate point(ast, env), to: Compiler.DataControl
defdelegate read_pin(ast, env), to: Compiler.PinControl defdelegate read_pin(ast, env), to: Compiler.PinControl
defdelegate resource_update(ast, env), to: Compiler.DataControl defdelegate resource(ast, env), to: Compiler.DataControl
defdelegate rpc_request(ast, env), to: Compiler.RPCRequest defdelegate rpc_request(ast, env), to: Compiler.RPCRequest
defdelegate sequence(ast, env), to: Compiler.Sequence defdelegate sequence(ast, env), to: Compiler.Sequence
defdelegate set_pin_io_mode(ast, env), to: Compiler.PinControl defdelegate set_pin_io_mode(ast, env), to: Compiler.PinControl
defdelegate set_servo_angle(ast, env), to: Compiler.PinControl defdelegate set_servo_angle(ast, env), to: Compiler.PinControl
defdelegate set_user_env(ast, env), to: Compiler.Farmware defdelegate set_user_env(ast, env), to: Compiler.Farmware
defdelegate take_photo(ast, env), to: Compiler.Farmware defdelegate take_photo(ast, env), to: Compiler.Farmware
defdelegate tool(ast, env), to: Compiler.DataControl
defdelegate toggle_pin(ast, env), to: Compiler.PinControl defdelegate toggle_pin(ast, env), to: Compiler.PinControl
defdelegate tool(ast, env), to: Compiler.DataControl
defdelegate unquote(:_if)(ast, env), to: Compiler.If
defdelegate update_farmware(ast, env), to: Compiler.Farmware defdelegate update_farmware(ast, env), to: Compiler.Farmware
defdelegate update_resource(ast, env), to: Compiler.UpdateResource
defdelegate variable_declaration(ast, env), to: Compiler.VariableDeclaration defdelegate variable_declaration(ast, env), to: Compiler.VariableDeclaration
defdelegate write_pin(ast, env), to: Compiler.PinControl defdelegate write_pin(ast, env), to: Compiler.PinControl
defdelegate zero(ast, env), to: Compiler.AxisControl defdelegate zero(ast, env), to: Compiler.AxisControl
@ -280,13 +274,13 @@ defmodule FarmbotCeleryScript.Compiler do
end end
defp print_compiled_code(compiled) do defp print_compiled_code(compiled) do
IO.puts("========") IO.puts("=== START ===")
compiled compiled
|> Macro.to_string() |> Macro.to_string()
|> Code.format_string!() |> Code.format_string!()
|> IO.puts() |> IO.puts()
IO.puts("========\n\n") IO.puts("=== END ===\n\n")
end end
end end

View File

@ -2,7 +2,7 @@ defmodule FarmbotCeleryScript.Compiler.IdentifierSanitizer do
@moduledoc """ @moduledoc """
Responsible for ensuring variable names in Sequences are clean. Responsible for ensuring variable names in Sequences are clean.
This is done because identifiers are `unquote`d and the user controls This is done because identifiers are `unquote`d and the user controls
the data inside them. To prevent things like the data inside them. To prevent things like
`"System.cmd("rm -rf /*/**")"` being evaluated, all identifiers `"System.cmd("rm -rf /*/**")"` being evaluated, all identifiers
are sanitized by prepending a token and hashing the value. are sanitized by prepending a token and hashing the value.
""" """

View File

@ -1,6 +1,19 @@
defmodule FarmbotCeleryScript.Compiler.DataControl do defmodule FarmbotCeleryScript.Compiler.DataControl do
alias FarmbotCeleryScript.Compiler alias FarmbotCeleryScript.Compiler
def resource(ast, _env) do
IO.puts("======")
IO.inspect(ast)
# %FarmbotCeleryScript.AST{
# args: %{resource_id: 0, resource_type: "Device"},
# body: [],
# comment: nil,
# kind: :resource,
# meta: nil
# }
raise "TODO: Pull resource from DB?"
end
# compiles coordinate # compiles coordinate
# Coordinate should return a vec3 # Coordinate should return a vec3
def coordinate(%{args: %{x: x, y: y, z: z}}, env) do def coordinate(%{args: %{x: x, y: y, z: z}}, env) do
@ -40,35 +53,4 @@ defmodule FarmbotCeleryScript.Compiler.DataControl do
) )
end end
end end
def resource_update(
%{
args: %{
resource_type: kind,
resource_id: id,
label: label,
value: value
},
body: body
},
env
) do
initial = %{label => value}
# Technically now body isn't supported by this node.
extra =
Map.new(body, fn %{args: %{label: label, data_value: value}} ->
{label, value}
end)
# Make sure the initial stuff higher most priority
params = Map.merge(extra, initial)
quote do
FarmbotCeleryScript.SysCalls.resource_update(
unquote(Compiler.compile_ast(kind, env)),
unquote(Compiler.compile_ast(id, env)),
unquote(Macro.escape(params))
)
end
end
end end

View File

@ -1,5 +1,6 @@
defmodule FarmbotCeleryScript.Compiler.Sequence do defmodule FarmbotCeleryScript.Compiler.Sequence do
import FarmbotCeleryScript.Compiler.Utils import FarmbotCeleryScript.Compiler.Utils
alias FarmbotCeleryScript.Compiler.IdentifierSanitizer
@iterables [:point_group, :every_point] @iterables [:point_group, :every_point]
@ -29,13 +30,7 @@ defmodule FarmbotCeleryScript.Compiler.Sequence do
def compile_sequence_iterable( def compile_sequence_iterable(
iterable_ast, iterable_ast,
%{ %{
args: args: %{locals: %{body: _} = locals} = sequence_args,
%{
locals:
%{
body: params
} = locals
} = sequence_args,
meta: sequence_meta meta: sequence_meta
} = sequence_ast, } = sequence_ast,
env env
@ -43,31 +38,6 @@ defmodule FarmbotCeleryScript.Compiler.Sequence do
sequence_name = sequence_name =
sequence_meta[:sequence_name] || sequence_args[:sequence_name] sequence_meta[:sequence_name] || sequence_args[:sequence_name]
# remove the iterable from the parameter applications,
# since it will be injected after this.
_params =
Enum.reduce(params, [], fn
# Remove point_group from parameter appls
%{
kind: :parameter_application,
args: %{data_value: %{kind: :point_group}}
},
acc ->
acc
# Remove every_point from parameter appls
%{
kind: :parameter_application,
args: %{data_value: %{kind: :every_point}}
},
acc ->
acc
# Everything else gets added back
ast, acc ->
acc ++ [ast]
end)
# will be a point_group or every_point node # will be a point_group or every_point node
group_ast = iterable_ast.args.data_value group_ast = iterable_ast.args.data_value
# check if it's a point_group first, then fall back to every_point # check if it's a point_group first, then fall back to every_point
@ -142,6 +112,37 @@ defmodule FarmbotCeleryScript.Compiler.Sequence do
end end
end end
def create_better_params(body, env) do
parameter_declarations =
Enum.reduce(env, %{}, fn
{key, value}, map ->
encoded_label = "#{key}"
if String.starts_with?(encoded_label, "unsafe_") do
Map.put(map, IdentifierSanitizer.to_string(encoded_label), value)
else
map
end
end)
Enum.reduce(body, parameter_declarations, fn ast, map ->
case ast do
%{kind: :parameter_application} ->
args = Map.fetch!(ast, :args)
label = Map.fetch!(args, :label)
Map.put(map, label, Map.fetch!(args, :data_value))
%{kind: :variable_declaration} ->
args = Map.fetch!(ast, :args)
label = Map.fetch!(args, :label)
Map.put(map, label, Map.fetch!(args, :data_value))
%{kind: :parameter_declaration} ->
map
end
end)
end
def compile_sequence( def compile_sequence(
%{args: %{locals: %{body: params}} = args, body: block, meta: meta}, %{args: %{locals: %{body: params}} = args, body: block, meta: meta},
env env
@ -150,6 +151,9 @@ defmodule FarmbotCeleryScript.Compiler.Sequence do
# The `params` side gets turned into # The `params` side gets turned into
# a keyword list. These `params` are passed in from a previous sequence. # a keyword list. These `params` are passed in from a previous sequence.
# The `body` side declares variables in _this_ scope. # The `body` side declares variables in _this_ scope.
# === DON'T USE THIS IN NEW CODE.
# SCHEDULED FOR DEPRECATION.
# USE `better_params` INSTEAD.
{params_fetch, body} = {params_fetch, body} =
Enum.reduce(params, {[], []}, fn ast, {params, body} = _acc -> Enum.reduce(params, {[], []}, fn ast, {params, body} = _acc ->
case ast do case ast do
@ -173,6 +177,8 @@ defmodule FarmbotCeleryScript.Compiler.Sequence do
steps = add_sequence_init_and_complete_logs(steps, sequence_name) steps = add_sequence_init_and_complete_logs(steps, sequence_name)
better_params = create_better_params(params, env)
[ [
quote location: :keep do quote location: :keep do
fn params -> fn params ->
@ -183,7 +189,7 @@ defmodule FarmbotCeleryScript.Compiler.Sequence do
# parent = Keyword.fetch!(params, :parent) # parent = Keyword.fetch!(params, :parent)
unquote_splicing(params_fetch) unquote_splicing(params_fetch)
unquote_splicing(assignments) unquote_splicing(assignments)
better_params = unquote(better_params)
# Unquote the remaining sequence steps. # Unquote the remaining sequence steps.
unquote(steps) unquote(steps)
end end

View File

@ -0,0 +1,61 @@
defmodule FarmbotCeleryScript.Compiler.UpdateResource do
alias FarmbotCeleryScript.{AST, DotProps}
def update_resource(%AST{args: args, body: body}, _env) do
quote location: :keep do
me = unquote(__MODULE__)
variable = unquote(Map.fetch!(args, :resource))
update = unquote(unpair(body, %{}))
# Go easy on the API...
case variable do
%AST{kind: :identifier} ->
args = Map.fetch!(variable, :args)
label = Map.fetch!(args, :label)
resource = Map.fetch!(better_params, label)
me.do_update(resource, update)
%AST{kind: :point} ->
me.do_update(variable.args, update)
%AST{kind: :resource} ->
me.do_update(variable.args, update)
res ->
raise "Resource error. Please notfiy support: #{inspect(res)}"
end
end
end
def do_update(%{pointer_id: id, pointer_type: kind}, update_params) do
FarmbotCeleryScript.SysCalls.update_resource(kind, id, update_params)
end
def do_update(%{resource_id: id, resource_type: kind}, update_params) do
FarmbotCeleryScript.SysCalls.update_resource(kind, id, update_params)
end
def do_update(%{args: %{pointer_id: id, pointer_type: k}}, update_params) do
FarmbotCeleryScript.SysCalls.update_resource(k, id, update_params)
end
def do_update(other, update) do
raise String.trim("""
MARK AS can only be used to mark resources like plants and devices.
It cannot be used on things like coordinates.
Ensure that your sequences and farm events us MARK AS on plants and not
coordinates (#{inspect(other)} / #{inspect(update)})
""")
end
defp unpair([pair | rest], acc) do
key = Map.fetch!(pair.args, :label)
val = Map.fetch!(pair.args, :value)
next_acc = Map.merge(acc, DotProps.create(key, val))
unpair(rest, next_acc)
end
defp unpair([], acc) do
acc
end
end

View File

@ -0,0 +1,22 @@
defmodule FarmbotCeleryScript.DotProps do
@dot "."
@doc ~S"""
Takes a "dotted" key and val.
Returns deeply nested hash.
## Examples
iex> create("foo.bar.baz", 321)
%{"foo" => %{"bar" => %{"baz" => 321}}}
iex> create("foo", "bar")
%{"foo" => "bar"}
"""
def create(dotted, val) do
[key | list] = dotted |> String.split(@dot) |> Enum.reverse()
Enum.reduce(list, %{key => val}, fn next_key, acc ->
%{next_key => acc}
end)
end
end

View File

@ -57,7 +57,6 @@ defmodule FarmbotCeleryScript.SysCalls do
speed :: number() speed :: number()
) :: ) ::
ok_or_error ok_or_error
# ?
@callback named_pin(named_pin_type :: String.t(), resource_id) :: @callback named_pin(named_pin_type :: String.t(), resource_id) ::
map() | integer | error() map() | integer | error()
@callback nothing() :: any() @callback nothing() :: any()
@ -69,7 +68,6 @@ defmodule FarmbotCeleryScript.SysCalls do
@callback toggle_pin(pin_num :: number()) :: ok_or_error @callback toggle_pin(pin_num :: number()) :: ok_or_error
@callback read_status() :: ok_or_error @callback read_status() :: ok_or_error
@callback reboot() :: ok_or_error @callback reboot() :: ok_or_error
@callback resource_update(String.t(), resource_id, map()) :: ok_or_error
@callback send_message(type :: String.t(), message :: String.t(), [atom]) :: @callback send_message(type :: String.t(), message :: String.t(), [atom]) ::
ok_or_error ok_or_error
@callback set_servo_angle(pin :: number(), value :: number()) :: ok_or_error @callback set_servo_angle(pin :: number(), value :: number()) :: ok_or_error
@ -95,6 +93,8 @@ defmodule FarmbotCeleryScript.SysCalls do
@callback find_points_via_group(String.t() | resource_id) :: %{ @callback find_points_via_group(String.t() | resource_id) :: %{
required(:point_ids) => [resource_id] required(:point_ids) => [resource_id]
} }
@callback update_resource(kind :: String.t(), resource_id, params :: map()) ::
ok_or_error
def find_points_via_group(sys_calls \\ @sys_calls, point_group_id) do def find_points_via_group(sys_calls \\ @sys_calls, point_group_id) do
point_group_or_error(sys_calls, :find_points_via_group, [point_group_id]) point_group_or_error(sys_calls, :find_points_via_group, [point_group_id])
@ -297,10 +297,6 @@ defmodule FarmbotCeleryScript.SysCalls do
ok_or_error(sys_calls, :reboot, []) ok_or_error(sys_calls, :reboot, [])
end end
def resource_update(sys_calls \\ @sys_calls, kind, id, params) do
ok_or_error(sys_calls, :resource_update, [kind, id, params])
end
def send_message(sys_calls \\ @sys_calls, kind, msg, channels) do def send_message(sys_calls \\ @sys_calls, kind, msg, channels) do
ok_or_error(sys_calls, :send_message, [kind, msg, channels]) ok_or_error(sys_calls, :send_message, [kind, msg, channels])
end end
@ -333,6 +329,10 @@ defmodule FarmbotCeleryScript.SysCalls do
ok_or_error(sys_calls, :zero, [axis]) ok_or_error(sys_calls, :zero, [axis])
end end
def update_resource(sys_calls \\ @sys_calls, kind, id, params) do
ok_or_error(sys_calls, :update_resource, [kind, id, params])
end
defp ok_or_error(sys_calls, fun, args) do defp ok_or_error(sys_calls, fun, args) do
case apply(sys_calls, fun, args) do case apply(sys_calls, fun, args) do
:ok -> :ok :ok -> :ok

View File

@ -119,10 +119,6 @@ defmodule FarmbotCeleryScript.SysCalls.Stubs do
@impl true @impl true
def reboot(), do: error(:reboot, []) def reboot(), do: error(:reboot, [])
@impl true
def resource_update(kind, resource_id, data),
do: error(:resource_update, [kind, resource_id, data])
@impl true @impl true
def send_message(type, message, channels), def send_message(type, message, channels),
do: error(:send_message, [type, message, channels]) do: error(:send_message, [type, message, channels])
@ -147,6 +143,10 @@ defmodule FarmbotCeleryScript.SysCalls.Stubs do
def write_pin(pin_num, pin_mode, pin_value), def write_pin(pin_num, pin_mode, pin_value),
do: error(:write_pin, [pin_num, pin_mode, pin_value]) do: error(:write_pin, [pin_num, pin_mode, pin_value])
@impl true
def update_resource(kind, id, params),
do: error(:update_resource, [kind, id, params])
@impl true @impl true
def zero(axis), do: error(:zero, [axis]) def zero(axis), do: error(:zero, [axis])

View File

@ -0,0 +1,4 @@
defmodule FarmbotCeleryScript.DotPropsTest do
use ExUnit.Case
doctest FarmbotCeleryScript.DotProps, import: true
end

View File

@ -47,8 +47,8 @@ defmodule FarmbotCeleryScript.CompilerGroupsTest do
canary_actual = :crypto.hash(:sha, Macro.to_string(result)) canary_actual = :crypto.hash(:sha, Macro.to_string(result))
canary_expected = canary_expected =
<<157, 69, 5, 38, 188, 78, 10, 183, 154, 99, 151, 193, 214, 208, 187, 130, <<136, 140, 48, 226, 216, 155, 178, 103, 244, 88, 225, 146, 130, 216, 125,
183, 73, 13, 48>> 72, 113, 195, 65, 1>>
# READ THE NOTE ABOVE IF THIS TEST FAILS!!! # READ THE NOTE ABOVE IF THIS TEST FAILS!!!
assert canary_expected == canary_actual assert canary_expected == canary_actual

View File

@ -79,7 +79,7 @@ defmodule FarmbotCeleryScript.CompilerTest do
end end
test "identifier sanitization" do test "identifier sanitization" do
label = "System.cmd(\"rm\", [\"-rf /*\"])" label = "System.cmd(\"echo\", [\"lol\"])"
value_ast = AST.Factory.new("coordinate", x: 1, y: 1, z: 1) value_ast = AST.Factory.new("coordinate", x: 1, y: 1, z: 1)
identifier_ast = AST.Factory.new("identifier", label: label) identifier_ast = AST.Factory.new("identifier", label: label)
@ -120,11 +120,19 @@ defmodule FarmbotCeleryScript.CompilerTest do
[ [
fn params -> fn params ->
_ = inspect(params) _ = inspect(params)
unsafe_U3lzdGVtLmNtZCgiZWNobyIsIFsibG9sIl0p = FarmbotCeleryScript.SysCalls.coordinate(1, 1, 1)
#{var_name} = better_params = %{
FarmbotCeleryScript.SysCalls.coordinate(1, 1, 1) "System.cmd(\\"echo\\", [\\"lol\\"])" => %FarmbotCeleryScript.AST{
args: %{x: 1, y: 1, z: 1},
body: [],
comment: nil,
kind: :coordinate,
meta: nil
}
}
[fn -> #{var_name} end] [fn -> unsafe_U3lzdGVtLmNtZCgiZWNobyIsIFsibG9sIl0p end]
end end
] ]
""") """)
@ -355,6 +363,114 @@ defmodule FarmbotCeleryScript.CompilerTest do
""") """)
end end
test "`update_resource`: " do
compiled =
"test/fixtures/mark_variable_removed.json"
|> File.read!()
|> Jason.decode!()
|> AST.decode()
|> compile()
assert compiled ==
strip_nl("""
[
fn params ->
_ = inspect(params)
unsafe_cGFyZW50 =
Keyword.get(params, :unsafe_cGFyZW50, FarmbotCeleryScript.SysCalls.coordinate(1, 2, 3))
better_params = %{}
[
fn ->
me = FarmbotCeleryScript.Compiler.UpdateResource
variable = %FarmbotCeleryScript.AST{
args: %{label: "parent"},
body: [],
comment: nil,
kind: :identifier,
meta: nil
}
update = %{"plant_stage" => "removed"}
case(variable) do
%AST{kind: :identifier} ->
args = Map.fetch!(variable, :args)
label = Map.fetch!(args, :label)
resource = Map.fetch!(better_params, label)
me.do_update(resource, update)
%AST{kind: :point} ->
me.do_update(variable.args(), update)
%AST{kind: :resource} ->
me.do_update(variable.args(), update)
res ->
raise("Resource error. Please notfiy support: \#{inspect(res)}")
end
end
]
end
]
""")
end
test "`update_resource`: Multiple fields of `resource` type." do
compiled =
"test/fixtures/update_resource_multi.json"
|> File.read!()
|> Jason.decode!()
|> AST.decode()
|> compile()
assert compiled ==
strip_nl("""
[
fn params ->
_ = inspect(params)
better_params = %{}
[
fn ->
me = FarmbotCeleryScript.Compiler.UpdateResource
variable = %FarmbotCeleryScript.AST{
args: %{resource_id: 23, resource_type: "Plant"},
body: [],
comment: nil,
kind: :resource,
meta: nil
}
update = %{"plant_stage" => "planted", "r" => 23}
case(variable) do
%AST{kind: :identifier} ->
args = Map.fetch!(variable, :args)
label = Map.fetch!(args, :label)
resource = Map.fetch!(better_params, label)
me.do_update(resource, update)
%AST{kind: :point} ->
me.do_update(variable.args(), update)
%AST{kind: :resource} ->
me.do_update(variable.args(), update)
res ->
raise("Resource error. Please notfiy support: \#{inspect(res)}")
end
end
]
end
]
""")
end
defp compile(ast) do defp compile(ast) do
ast ast
|> Compiler.compile_ast([]) |> Compiler.compile_ast([])

View File

@ -4,7 +4,7 @@ defmodule FarmbotCeleryScript.Corpus.NodeTest do
test "inspect" do test "inspect" do
a = a =
"Sequence(version, locals) [calibrate, change_ownership, check_updates, emergency_lock, emergency_unlock, execute, execute_script, factory_reset, find_home, flash_firmware, home, install_farmware, install_first_party_farmware, _if, move_absolute, move_relative, power_off, read_pin, read_status, reboot, remove_farmware, resource_update, send_message, set_servo_angle, set_user_env, sync, take_photo, toggle_pin, update_farmware, wait, write_pin, zero]" "Sequence(version, locals) [assertion, calibrate, change_ownership, check_updates, emergency_lock, emergency_unlock, execute, execute_script, factory_reset, find_home, flash_firmware, home, install_farmware, install_first_party_farmware, _if, move_absolute, move_relative, power_off, read_pin, read_status, reboot, remove_farmware, update_resource, send_message, set_servo_angle, set_user_env, sync, take_photo, toggle_pin, update_farmware, wait, write_pin, zero]"
b = inspect(Corpus.sequence()) b = inspect(Corpus.sequence())
assert a == b assert a == b

View File

@ -0,0 +1,48 @@
{
"args": {
"locals": {
"args": {},
"body": [
{
"args": {
"default_value": {
"args": {
"x": 1,
"y": 2,
"z": 3
},
"kind": "coordinate"
},
"label": "parent"
},
"kind": "parameter_declaration"
}
],
"kind": "scope_declaration"
},
"version": -999
},
"body": [
{
"args": {
"resource": {
"args": {
"label": "parent"
},
"kind": "identifier"
}
},
"body": [
{
"args": {
"label": "meta.my_prop",
"value": "whatever"
},
"kind": "pair"
}
],
"kind": "update_resource"
}
],
"kind": "sequence"
}

View File

@ -0,0 +1,48 @@
{
"args": {
"locals": {
"args": {},
"body": [
{
"args": {
"default_value": {
"args": {
"x": 1,
"y": 2,
"z": 3
},
"kind": "coordinate"
},
"label": "parent"
},
"kind": "parameter_declaration"
}
],
"kind": "scope_declaration"
},
"version": -999
},
"body": [
{
"args": {
"resource": {
"args": {
"label": "parent"
},
"kind": "identifier"
}
},
"body": [
{
"args": {
"label": "plant_stage",
"value": "removed"
},
"kind": "pair"
}
],
"kind": "update_resource"
}
],
"kind": "sequence"
}

View File

@ -0,0 +1,34 @@
{
"args": {
"locals": {
"args": {},
"body": [],
"kind": "scope_declaration"
},
"version": -999
},
"body": [
{
"args": {
"resource": {
"args": {
"resource_id": 0,
"resource_type": "Device"
},
"kind": "resource"
}
},
"body": [
{
"args": {
"label": "mounted_tool_id",
"value": 12161
},
"kind": "pair"
}
],
"kind": "update_resource"
}
],
"kind": "sequence"
}

View File

@ -0,0 +1,41 @@
{
"args": {
"locals": {
"args": {},
"body": [],
"kind": "scope_declaration"
},
"version": -999
},
"body": [
{
"args": {
"resource": {
"args": {
"resource_id": 23,
"resource_type": "Plant"
},
"kind": "resource"
}
},
"body": [
{
"args": {
"label": "plant_stage",
"value": "planted"
},
"kind": "pair"
},
{
"args": {
"label": "r",
"value": 23
},
"kind": "pair"
}
],
"kind": "update_resource"
}
],
"kind": "sequence"
}

View File

@ -46,14 +46,7 @@ config :farmbot_core, FarmbotCore.EctoMigrator,
"beta" "beta"
) )
config :farmbot_core, FarmbotCore.FirmwareTTYDetector, expected_names: [] config :farmbot_firmware, FarmbotFirmware, reset: FarmbotCore.FirmwareResetter
config :farmbot_core, FarmbotCore.FirmwareOpenTask, attempt_threshold: 5
config :farmbot_firmware, FarmbotFirmware, reset: FarmbotFirmware.NullReset
config :farmbot_core, FarmbotCore.AssetWorker.FarmbotCore.Asset.FbosConfig,
firmware_flash_attempt_threshold: 5
import_config "ecto.exs" import_config "ecto.exs"
import_config "logger.exs" import_config "logger.exs"

View File

@ -2,8 +2,3 @@ use Mix.Config
config :farmbot_celery_script, FarmbotCeleryScript.SysCalls, config :farmbot_celery_script, FarmbotCeleryScript.SysCalls,
sys_calls: FarmbotCeleryScript.SysCalls.Stubs sys_calls: FarmbotCeleryScript.SysCalls.Stubs
config :farmbot_core, FarmbotCore.FirmwareOpenTask, attempt_threshold: 5
config :farmbot_core, FarmbotCore.AssetWorker.FarmbotCore.Asset.FbosConfig,
firmware_flash_attempt_threshold: 5

View File

@ -27,7 +27,10 @@ defmodule FarmbotCore do
FarmbotCore.FirmwareOpenTask, FarmbotCore.FirmwareOpenTask,
FarmbotCore.FirmwareEstopTimer, FarmbotCore.FirmwareEstopTimer,
# Also error handling for a transport not starting ? # Also error handling for a transport not starting ?
{FarmbotFirmware, transport: FarmbotFirmware.StubTransport, side_effects: FarmbotCore.FirmwareSideEffects}, {FarmbotFirmware,
transport: FarmbotFirmware.StubTransport,
side_effects: FarmbotCore.FirmwareSideEffects,
reset: FarmbotCore.FirmwareResetter},
FarmbotCeleryScript.Scheduler FarmbotCeleryScript.Scheduler
] ]
config = (Application.get_env(:farmbot_ext, __MODULE__) || []) config = (Application.get_env(:farmbot_ext, __MODULE__) || [])

View File

@ -6,15 +6,15 @@ defmodule FarmbotCore.Asset do
""" """
alias FarmbotCore.Asset.{ alias FarmbotCore.Asset.{
Repo, CriteriaRetriever,
Device, Device,
DeviceCert, DeviceCert,
FarmwareEnv,
FirstPartyFarmware,
FarmwareInstallation,
FarmEvent, FarmEvent,
FarmwareEnv,
FarmwareInstallation,
FbosConfig, FbosConfig,
FirmwareConfig, FirmwareConfig,
FirstPartyFarmware,
Peripheral, Peripheral,
PinBinding, PinBinding,
Point, Point,
@ -22,11 +22,11 @@ defmodule FarmbotCore.Asset do
PublicKey, PublicKey,
Regimen, Regimen,
RegimenInstance, RegimenInstance,
Sequence, Repo,
Sensor, Sensor,
SensorReading, SensorReading,
Sequence,
Tool, Tool,
CriteriaRetriever
} }
alias FarmbotCore.AssetSupervisor alias FarmbotCore.AssetSupervisor
@ -251,8 +251,24 @@ defmodule FarmbotCore.Asset do
end end
def update_point(point, params) do def update_point(point, params) do
point # TODO: RC 8 MAY 2020 - We need to hard refresh the point.
|> Point.changeset(params) # The CSVM appears to be caching resources. This leads
# to problems when a user runs a sequence that has two
# MARK AS steps.
# NOTE: Updating the `meta` attribute is a _replace_ action
# by default, not a merge action.
# MORE NOTES: Mixed keys (symbol vs. string) will crash this FN.
# Let's just stringify everything...
new_meta = params[:meta] || params["meta"] || %{}
old_meta = point.meta || %{}
updated_meta = Map.merge(old_meta, new_meta)
clean_params = params
|> Map.merge(%{meta: updated_meta})
|> Enum.map(fn {k, v} -> {"#{k}", v} end)
|> Map.new()
Repo.get_by(Point, id: point.id)
|> Point.changeset(clean_params)
|> Repo.update() |> Repo.update()
end end

View File

@ -132,15 +132,14 @@ defmodule FarmbotCore.Asset.CriteriaRetriever do
end end
defp stage_1_day_field({pg, accum}) do defp stage_1_day_field({pg, accum}) do
day_criteria = pg.criteria["day"] || %{} day_criteria = pg.criteria["day"] || pg.criteria[:day] || %{}
days = day_criteria["days_ago"] || 0 days = day_criteria["days_ago"] || day_criteria[:days_ago] || 0
op = day_criteria["op"] || day_criteria[:op] || "<"
time = Timex.shift(Timex.now(), days: -1 * days)
if days == 0 do if days == 0 do
{ pg, accum } { pg, accum }
else else
op = day_criteria["op"] || "<"
time = Timex.shift(Timex.now(), days: -1 * days)
inverted_op = if op == ">" do "<" else ">" end inverted_op = if op == ">" do "<" else ">" end
{ pg, accum ++ [{"created_at", inverted_op, time}] } { pg, accum ++ [{"created_at", inverted_op, time}] }

View File

@ -37,6 +37,7 @@ defmodule FarmbotCore.Asset.Point do
meta: point.meta, meta: point.meta,
name: point.name, name: point.name,
plant_stage: point.plant_stage, plant_stage: point.plant_stage,
created_at: point.created_at,
planted_at: point.planted_at, planted_at: point.planted_at,
pointer_type: point.pointer_type, pointer_type: point.pointer_type,
radius: point.radius, radius: point.radius,

View File

@ -40,17 +40,17 @@ defmodule FarmbotCore.Asset.Private do
# Because sqlite can't test unique constraints before a transaction, if this function gets called for # Because sqlite can't test unique constraints before a transaction, if this function gets called for
# the same asset more than once asyncronously, the asset can be marked dirty twice at the same time # the same asset more than once asyncronously, the asset can be marked dirty twice at the same time
# causing the `unique constraint` error to happen in either `ecto` OR `sqlite`. I've # causing the `unique constraint` error to happen in either `ecto` OR `sqlite`. I've
# caught both errors here as they are both essentially the same thing, and can be safely # caught both errors here as they are both essentially the same thing, and can be safely
# discarded. Doing an `insert_or_update/1` (without the bang) can still result in the sqlite # discarded. Doing an `insert_or_update/1` (without the bang) can still result in the sqlite
# error being thrown. # error being thrown.
changeset = LocalMeta.changeset(local_meta, Map.merge(params, %{table: table, status: "dirty"})) changeset = LocalMeta.changeset(local_meta, Map.merge(params, %{table: table, status: "dirty"}))
try do try do
Repo.insert_or_update!(changeset) Repo.insert_or_update!(changeset)
catch catch
:error, %Sqlite.DbConnection.Error{ :error, %Sqlite.DbConnection.Error{
message: "UNIQUE constraint failed: local_metas.table, local_metas.asset_local_id", message: "UNIQUE constraint failed: local_metas.table, local_metas.asset_local_id",
sqlite: %{code: :constraint} sqlite: %{code: :constraint}
} -> } ->
Logger.warn """ Logger.warn """
Caught race condition marking data as dirty (sqlite) Caught race condition marking data as dirty (sqlite)
table: #{inspect(table)} table: #{inspect(table)}
@ -59,10 +59,10 @@ defmodule FarmbotCore.Asset.Private do
Ecto.Changeset.apply_changes(changeset) Ecto.Changeset.apply_changes(changeset)
:error, %Ecto.InvalidChangesetError{ :error, %Ecto.InvalidChangesetError{
changeset: %{ changeset: %{
action: :insert, action: :insert,
errors: [ errors: [
table: {"LocalMeta already exists.", [ table: {"LocalMeta already exists.", [
validation: :unsafe_unique, validation: :unsafe_unique,
fields: [:table, :asset_local_id] fields: [:table, :asset_local_id]
]} ]}
]} ]}
@ -73,7 +73,7 @@ defmodule FarmbotCore.Asset.Private do
id: #{inspect(asset.local_id)} id: #{inspect(asset.local_id)}
""" """
Ecto.Changeset.apply_changes(changeset) Ecto.Changeset.apply_changes(changeset)
type, reason -> type, reason ->
FarmbotCore.Logger.error 1, """ FarmbotCore.Logger.error 1, """
Caught unexpected error marking data as dirty Caught unexpected error marking data as dirty
table: #{inspect(table)} table: #{inspect(table)}

View File

@ -11,16 +11,9 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.FbosConfig do
alias FarmbotCore.{Asset.FbosConfig, BotState, Config} alias FarmbotCore.{Asset.FbosConfig, BotState, Config}
import FarmbotFirmware.PackageUtils, only: [package_to_string: 1] import FarmbotFirmware.PackageUtils, only: [package_to_string: 1]
@firmware_flash_attempt_threshold Application.get_env(:farmbot_core, __MODULE__)[:firmware_flash_attempt_threshold] @firmware_flash_attempt_threshold Application.get_env(:farmbot_core, __MODULE__)[:firmware_flash_attempt_threshold] || 5
@firmware_flash_timeout Application.get_env(:farmbot_core, __MODULE__)[:firmware_flash_timeout] || 5000 @firmware_flash_timeout Application.get_env(:farmbot_core, __MODULE__)[:firmware_flash_timeout] || 5000
@disable_firmware_io_logs_timeout Application.get_env(:farmbot_core, __MODULE__)[:disable_firmware_io_logs_timeout] || 300000 @disable_firmware_io_logs_timeout Application.get_env(:farmbot_core, __MODULE__)[:disable_firmware_io_logs_timeout] || 300000
@firmware_flash_attempt_threshold || Mix.raise """
Firmware open attempt threshold not configured:
config :farmbot_core, #{__MODULE__}, [
firmware_flash_attempt_threshold: :infinity
]
"""
@impl FarmbotCore.AssetWorker @impl FarmbotCore.AssetWorker
def preload(%FbosConfig{}), do: [] def preload(%FbosConfig{}), do: []

View File

@ -9,14 +9,7 @@ defmodule FarmbotCore.FirmwareOpenTask do
require FarmbotCore.Logger require FarmbotCore.Logger
alias FarmbotFirmware.{UARTTransport, StubTransport} alias FarmbotFirmware.{UARTTransport, StubTransport}
alias FarmbotCore.{Asset, Config} alias FarmbotCore.{Asset, Config}
@attempt_threshold Application.get_env(:farmbot_core, __MODULE__)[:attempt_threshold] @attempt_threshold Application.get_env(:farmbot_core, __MODULE__)[:attempt_threshold] || 5
@attempt_threshold || Mix.raise """
Firmware open attempt threshold not configured:
config :farmbot_core, FarmbotCore.FirmwareOpenTask, [
attempt_threshold: 10
]
"""
@doc false @doc false
def start_link(args, opts \\ [name: __MODULE__]) do def start_link(args, opts \\ [name: __MODULE__]) do
@ -25,7 +18,10 @@ defmodule FarmbotCore.FirmwareOpenTask do
@doc false @doc false
def swap_transport(tty) do def swap_transport(tty) do
Application.put_env(:farmbot_firmware, FarmbotFirmware, transport: UARTTransport, device: tty) Application.put_env(:farmbot_firmware, FarmbotFirmware,
transport: UARTTransport,
device: tty,
reset: FarmbotCore.FirmwareResetter)
# Swap transport on FW module. # Swap transport on FW module.
# Close tranpsort if it is open currently. # Close tranpsort if it is open currently.
_ = FarmbotFirmware.close_transport() _ = FarmbotFirmware.close_transport()
@ -33,7 +29,9 @@ defmodule FarmbotCore.FirmwareOpenTask do
end end
def unswap_transport() do def unswap_transport() do
Application.put_env(:farmbot_firmware, FarmbotFirmware, transport: StubTransport) Application.put_env(:farmbot_firmware, FarmbotFirmware,
transport: StubTransport,
reset: FarmbotCore.FirmwareResetter)
# Swap transport on FW module. # Swap transport on FW module.
# Close tranpsort if it is open currently. # Close tranpsort if it is open currently.
_ = FarmbotFirmware.close_transport() _ = FarmbotFirmware.close_transport()

View File

@ -0,0 +1,46 @@
defmodule FarmbotCore.FirmwareResetter do
if Code.ensure_compiled?(Circuits.GPIO) do
@gpio Circuits.GPIO
else
@gpio nil
end
alias FarmbotCore.Asset
require FarmbotCore.Logger
def reset(package \\ nil) do
pkg = package || Asset.fbos_config(:firmware_hardware)
FarmbotCore.Logger.debug(3, "Attempting to retrieve #{pkg} reset function.")
{:ok, fun} = find_reset_fun(pkg)
fun.()
end
def find_reset_fun("express_k10") do
FarmbotCore.Logger.debug(3, "Using special express reset function")
{:ok, fn -> express_reset_fun() end}
end
def find_reset_fun(_) do
FarmbotCore.Logger.debug(3, "Using default reset function")
{:ok, fn -> :ok end}
end
def express_reset_fun() do
try do
gpio_module = @gpio
FarmbotCore.Logger.debug(3, "Begin MCU reset")
{:ok, gpio} = gpio_module.open(19, :output)
:ok = gpio_module.write(gpio, 0)
:ok = gpio_module.write(gpio, 1)
Process.sleep(1000)
:ok = gpio_module.write(gpio, 0)
FarmbotCore.Logger.debug(3, "Finish MCU Reset")
:ok
rescue
ex ->
message = Exception.message(ex)
msg = "Express reset failed #{message}"
FarmbotCore.Logger.error(3, msg)
{:error, msg}
end
end
end

View File

@ -3,17 +3,14 @@ defmodule FarmbotCore.FirmwareTTYDetector do
require Logger require Logger
alias Circuits.UART alias Circuits.UART
@expected_names Application.get_env(:farmbot_core, __MODULE__)[:expected_names]
@expected_names ||
Mix.raise("""
Please configure `expected_names` for TTYDetector.
config :farmbot_core, FarmbotCore.FirmwareTTYDetector,
expected_names: ["ttyS0", "ttyNotReal"]
""")
@error_retry_ms 5_000 @error_retry_ms 5_000
if System.get_env("FARMBOT_TTY") do
@expected_names ["ttyUSB0", "ttyAMA0", "ttyACM0", System.get_env("FARMBOT_TTY")]
else
@expected_names ["ttyUSB0", "ttyAMA0", "ttyACM0"]
end
@doc "Gets the detected TTY" @doc "Gets the detected TTY"
def tty(server \\ __MODULE__) do def tty(server \\ __MODULE__) do
GenServer.call(server, :tty) GenServer.call(server, :tty)
@ -53,7 +50,6 @@ defmodule FarmbotCore.FirmwareTTYDetector do
if farmbot_tty?(name) do if farmbot_tty?(name) do
{:noreply, name} {:noreply, name}
else else
# Logger.warn("#{name} is not an expected Farmbot Firmware TTY")
{:noreply, state, {:continue, rest}} {:noreply, state, {:continue, rest}}
end end
end end

View File

@ -124,8 +124,6 @@ defmodule FarmbotCore.Asset.CriteriaRetrieverTest do
expect(Timex, :now, fn -> @now end) expect(Timex, :now, fn -> @now end)
pg = point_group_with_fake_points() pg = point_group_with_fake_points()
# This one is _almost_ a perfect match,
# but the meta field is a miss.
point!(%{ point!(%{
id: 888, id: 888,
created_at: @five_days_ago, created_at: @five_days_ago,
@ -525,6 +523,37 @@ defmodule FarmbotCore.Asset.CriteriaRetrieverTest do
assert Enum.count(ids) == 1 assert Enum.count(ids) == 1
end end
test "edge case: Retrieves by `day` criteria only" do
Repo.delete_all(PointGroup)
Repo.delete_all(Point)
days_ago4 = Timex.shift(@now, days: -4)
days_ago2 = Timex.shift(@now, days: -2)
expect(Timex, :now, fn -> @now end)
point!(%{id: 1, pointer_type: "Plant", created_at: days_ago4})
p2 = point!(%{id: 2, pointer_type: "Plant", created_at: days_ago2})
pg1 = %PointGroup{
id: 212,
created_at: Timex.shift(@now, hours: -1),
updated_at: Timex.shift(@now, hours: -1),
name: "Less than 2 days ago",
point_ids: [],
sort_type: "yx_descending",
criteria: %{
day: %{"op" => "<", "days_ago" => 3},
string_eq: %{},
number_eq: %{},
number_lt: %{},
number_gt: %{}
}
}
ids = CriteriaRetriever.run(pg1) |> Enum.map(fn p -> p.id end)
assert Enum.count(ids) == 1
assert Enum.member?(ids, p2.id)
end
test "edge case: Filter by slot direction" do test "edge case: Filter by slot direction" do
Repo.delete_all(PointGroup) Repo.delete_all(PointGroup)
Repo.delete_all(Point) Repo.delete_all(Point)

View File

@ -2,8 +2,6 @@ use Mix.Config
config :logger, handle_otp_reports: true, handle_sasl_reports: true config :logger, handle_otp_reports: true, handle_sasl_reports: true
config :farmbot_firmware, FarmbotFirmware, reset: FarmbotFirmware.NullReset
# TODO(Rick) We probably don't need to use this anymore now that Mox is a thing. # TODO(Rick) We probably don't need to use this anymore now that Mox is a thing.
config :farmbot_celery_script, FarmbotCeleryScript.SysCalls, config :farmbot_celery_script, FarmbotCeleryScript.SysCalls,
sys_calls: FarmbotCeleryScript.SysCalls.Stubs sys_calls: FarmbotCeleryScript.SysCalls.Stubs

View File

@ -39,8 +39,6 @@ config :farmbot_core, FarmbotCore.EctoMigrator,
default_currently_on_beta: default_currently_on_beta:
String.contains?(to_string(:os.cmd('git rev-parse --abbrev-ref HEAD')), "beta") String.contains?(to_string(:os.cmd('git rev-parse --abbrev-ref HEAD')), "beta")
config :farmbot_core, FarmbotCore.FirmwareTTYDetector, expected_names: []
config :farmbot_core, FarmbotCore.FirmwareOpenTask, attempt_threshold: 0 config :farmbot_core, FarmbotCore.FirmwareOpenTask, attempt_threshold: 0
config :farmbot_core, FarmbotCore.AssetWorker.FarmbotCore.Asset.FbosConfig, config :farmbot_core, FarmbotCore.AssetWorker.FarmbotCore.Asset.FbosConfig,

View File

@ -6,8 +6,9 @@ defmodule FarmbotExt.API.DirtyWorker do
import API.View, only: [render: 2] import API.View, only: [render: 2]
require Logger require Logger
require FarmbotCore.Logger
use GenServer use GenServer
@timeout 10000 @timeout 500
# these resources can't be accessed by `id`. # these resources can't be accessed by `id`.
@singular [ @singular [
@ -34,84 +35,69 @@ defmodule FarmbotExt.API.DirtyWorker do
@impl GenServer @impl GenServer
def init(args) do def init(args) do
# Logger.disable(self())
module = Keyword.fetch!(args, :module) module = Keyword.fetch!(args, :module)
timeout = Keyword.get(args, :timeout, @timeout) Process.send_after(self(), :do_work, @timeout)
{:ok, %{module: module, timeout: timeout}, timeout} {:ok, %{module: module}}
end end
@impl GenServer @impl GenServer
def handle_info(:timeout, %{module: module} = state) do def handle_info(:do_work, %{module: module} = state) do
dirty = Private.list_dirty(module) Process.sleep(@timeout)
local = Private.list_local(module) list = Enum.uniq(Private.list_dirty(module) ++ Private.list_local(module))
{:noreply, state, {:continue, Enum.uniq(dirty ++ local)}}
unless has_race_condition?(module, list) do
Enum.map(list, fn dirty -> work(dirty, module) end)
end
Process.send_after(self(), :do_work, @timeout)
{:noreply, state}
end end
@impl GenServer def work(dirty, module) do
def handle_continue([], state) do # Go easy on the API
{:noreply, state, state.timeout} Process.sleep(333)
end
def handle_continue([dirty | rest], %{module: module} = state) do case http_request(dirty, module) do
# Logger.info("[#{module} #{dirty.local_id} #{inspect(self())}] Handling dirty data")
case http_request(dirty, state) do
# Valid data # Valid data
{:ok, %{status: s, body: body}} when s > 199 and s < 300 -> {:ok, %{status: s, body: body}} when s > 199 and s < 300 ->
# Logger.debug( dirty |> module.changeset(body) |> handle_changeset(module)
# "[#{module} #{dirty.local_id} #{inspect(self())}] HTTP request complete: #{s} ok"
# )
dirty |> module.changeset(body) |> handle_changeset(rest, state)
# Invalid data # Invalid data
{:ok, %{status: s, body: %{} = body}} when s > 399 and s < 500 -> {:ok, %{status: s, body: %{} = body}} when s > 399 and s < 500 ->
# Logger.debug( FarmbotCore.Logger.error(2, "HTTP Error #{s}. #{inspect(body)}")
# "[#{module} #{dirty.local_id} #{inspect(self())}] HTTP request complete: #{s} error+body"
# )
changeset = module.changeset(dirty) changeset = module.changeset(dirty)
Enum.reduce(body, changeset, fn {key, val}, changeset -> Enum.reduce(body, changeset, fn {key, val}, changeset ->
Ecto.Changeset.add_error(changeset, key, val) Ecto.Changeset.add_error(changeset, key, val)
end) end)
|> handle_changeset(rest, state) |> handle_changeset(module)
# Invalid data, but the API didn't say why # Invalid data, but the API didn't say why
{:ok, %{status: s, body: _body}} when s > 399 and s < 500 -> {:ok, %{status: s, body: _body}} when s > 399 and s < 500 ->
# Logger.debug( FarmbotCore.Logger.error(2, "HTTP Error #{s}. #{inspect(dirty)}")
# "[#{module} #{dirty.local_id} #{inspect(self())}] HTTP request complete: #{s} error"
# )
module.changeset(dirty) module.changeset(dirty)
|> Map.put(:valid?, false) |> Map.put(:valid?, false)
|> handle_changeset(rest, state) |> handle_changeset(module)
# HTTP Error. (500, network error, timeout etc.) # HTTP Error. (500, network error, timeout etc.)
error -> error ->
Logger.error( FarmbotCore.Logger.error(
"[#{module} #{dirty.local_id} #{inspect(self())}] HTTP Error: #{state.module} #{ 2,
"[#{module} #{dirty.local_id} #{inspect(self())}] HTTP Error: #{module} #{
inspect(error) inspect(error)
}" }"
) )
{:noreply, state, @timeout}
end end
end end
# If the changeset was valid, update the record. # If the changeset was valid, update the record.
def handle_changeset(%{valid?: true} = changeset, rest, state) do def handle_changeset(%{valid?: true} = changeset, _module) do
# Logger.info("Successfully synced: #{state.module}") Private.mark_clean!(Repo.update!(changeset))
:ok
Repo.update!(changeset)
|> Private.mark_clean!()
{:noreply, state, {:continue, rest}}
end end
# If the changeset was invalid, delete the record. def handle_changeset(%{valid?: false, data: data} = changeset, module) do
# TODO(Connor) - Update the dirty field here, upload to rollbar?
def handle_changeset(%{valid?: false, data: data} = changeset, rest, state) do
message = message =
Enum.map(changeset.errors, fn Enum.map(changeset.errors, fn
{key, {msg, _meta}} when is_binary(key) -> "\t#{key}: #{msg}" {key, {msg, _meta}} when is_binary(key) -> "\t#{key}: #{msg}"
@ -119,29 +105,64 @@ defmodule FarmbotExt.API.DirtyWorker do
end) end)
|> Enum.join("\n") |> Enum.join("\n")
Logger.error("Failed to sync: #{state.module} \n #{message}") FarmbotCore.Logger.error(3, "Failed to sync: #{module} \n #{message}")
_ = Repo.delete!(data) _ = Repo.delete!(data)
{:noreply, state, {:continue, rest}} :ok
end end
defp http_request(%{id: nil} = dirty, state) do defp http_request(%{id: nil} = dirty, module) do
# Logger.debug("#{state.module} clean request (post)") path = module.path()
path = state.module.path() data = render(module, dirty)
data = render(state.module, dirty)
API.post(API.client(), path, data) API.post(API.client(), path, data)
end end
defp http_request(dirty, %{module: module} = state) when module in @singular do defp http_request(dirty, module) when module in @singular do
# Logger.debug("#{state.module} dirty request (patch)") path = path = module.path()
path = path = state.module.path() data = render(module, dirty)
data = render(state.module, dirty)
API.patch(API.client(), path, data) API.patch(API.client(), path, data)
end end
defp http_request(dirty, state) do defp http_request(dirty, module) do
# Logger.debug("#{state.module} dirty request (patch)") path = Path.join(module.path(), to_string(dirty.id))
path = Path.join(state.module.path(), to_string(dirty.id)) data = render(module, dirty)
data = render(state.module, dirty)
API.patch(API.client(), path, data) API.patch(API.client(), path, data)
end end
# This is a fix for a race condtion. The root cause is unknown
# as of 18 May 2020. The problem is that records are marked
# diry _before_ the dirty data is saved. That means that FBOS
# knows a record has changed, but for a brief moment, it only
# has the old copy of the record (not the changes).
# Because of this race condtion,
# The race condition:
#
# * Is nondeterministic
# * Happens frequently when running many MARK AS steps in one go.
# * Happens frequently when Erlang VM only has one thread
# * Ie: `iex --erl '+S 1 +A 1' -S mix`
# * Happens frequently when @timeout is decreased to `1`.
#
# This function PREVENTS CORRUPTION OF API DATA. It can be
# removed once the root cause of the data race is determined.
# - RC 18 May 2020
def has_race_condition?(module, list) do
Enum.find(list, fn item ->
if item.id do
if item == Repo.get_by(module, id: item.id) do
# This item is OK - no race condition.
false
else
# There was a race condtion. We probably can't trust
# any of the data in this list. We need to wait and
# try again later.
Process.sleep(@timeout * 3)
true
end
else
# This item only exists on the FBOS side.
# It will never be affected by the data race condtion.
false
end
end)
end
end end

View File

@ -21,12 +21,12 @@ timeout = System.get_env("EXUNIT_TIMEOUT") || "5000"
System.put_env("LOG_SILENCE", "true") System.put_env("LOG_SILENCE", "true")
ExUnit.start(assert_receive_timeout: String.to_integer(timeout)) ExUnit.start(assert_receive_timeout: String.to_integer(timeout))
# Use this to stub out calls to `state.reset.reset()` in firmware.
defmodule StubReset do
def reset(), do: :ok
end
defmodule Helpers do defmodule Helpers do
# Maybe I don't need this?
# Maybe I could use `start_supervised`?
# https://hexdocs.pm/ex_unit/ExUnit.Callbacks.html#start_supervised/2
@wait_time 180 @wait_time 180
# Base case: We have a pid # Base case: We have a pid
def wait_for(pid) when is_pid(pid), do: check_on_mbox(pid) def wait_for(pid) when is_pid(pid), do: check_on_mbox(pid)

View File

@ -107,8 +107,7 @@ defmodule FarmbotFirmware do
:command_queue, :command_queue,
:caller_pid, :caller_pid,
:current, :current,
:reset, :reset
:reset_pid
] ]
@type state :: %State{ @type state :: %State{
@ -123,8 +122,7 @@ defmodule FarmbotFirmware do
command_queue: [{pid(), GCODE.t()}], command_queue: [{pid(), GCODE.t()}],
caller_pid: nil | pid, caller_pid: nil | pid,
current: nil | GCODE.t(), current: nil | GCODE.t(),
reset: module(), reset: module()
reset_pid: nil | pid()
} }
@doc """ @doc """
@ -202,16 +200,7 @@ defmodule FarmbotFirmware do
args = Keyword.merge(args, global) args = Keyword.merge(args, global)
transport = Keyword.fetch!(args, :transport) transport = Keyword.fetch!(args, :transport)
side_effects = Keyword.get(args, :side_effects) side_effects = Keyword.get(args, :side_effects)
# This is probably the cause of reset = Keyword.fetch!(args, :reset)
# https://github.com/FarmBot/farmbot_os/issues/1111
# FarmbotFirmware.NullReset (RPi3? Safe default?)
# -OR-
# FarmbotOS.Platform.Target.FirmwareReset.GPIO (RPi0, RPi)
# -OR-
# Use Application.get_env to find target?
# probably?
reset = Keyword.get(args, :reset) || FarmbotFirmware.NullReset
# Add an anon function that transport implementations should call. # Add an anon function that transport implementations should call.
fw = self() fw = self()
fun = fn {_, _} = code -> GenServer.cast(fw, code) end fun = fn {_, _} = code -> GenServer.cast(fw, code) end
@ -225,7 +214,6 @@ defmodule FarmbotFirmware do
side_effects: side_effects, side_effects: side_effects,
status: :transport_boot, status: :transport_boot,
reset: reset, reset: reset,
reset_pid: nil,
command_queue: [], command_queue: [],
configuration_queue: [] configuration_queue: []
} }
@ -242,24 +230,6 @@ defmodule FarmbotFirmware do
GenServer.stop(state.transport_pid) GenServer.stop(state.transport_pid)
end end
def handle_info(:timeout, %{status: :transport_boot, reset_pid: nil} = state) do
case GenServer.start_link(state.reset, state.transport_args,
name: state.reset
) do
{:ok, pid} ->
{:noreply, %{state | reset_pid: pid}}
# TODO(Rick): I have no idea what's going on here.
{:error, {:already_started, pid}} ->
{:noreply, %{state | reset_pid: pid}}
error ->
Logger.error("Error starting Firmware Reset: #{inspect(error)}")
Process.send_after(self(), :timeout, @transport_init_error_retry_ms)
{:noreply, state}
end
end
# This will be the first message received right after `init/1` # This will be the first message received right after `init/1`
# It should try to open a transport every `transport_init_error_retry_ms` # It should try to open a transport every `transport_init_error_retry_ms`
# until success. # until success.

View File

@ -1,15 +0,0 @@
defmodule FarmbotFirmware.NullReset do
@moduledoc """
Does nothing in reference to resetting the firmware port
"""
@behaviour FarmbotFirmware.Reset
use GenServer
@impl FarmbotFirmware.Reset
def reset(), do: :ok
@impl GenServer
def init(_args) do
{:ok, %{}}
end
end

View File

@ -553,6 +553,6 @@ defmodule FarmbotFirmware.Param do
def format_bool(val) when val == 1, do: true def format_bool(val) when val == 1, do: true
def format_bool(val) when val == 0, do: false def format_bool(val) when val == 0, do: false
def format_high_low_inverted(val) when val == 0, do: "HIGH" def format_high_low_inverted(val) when val == 0, do: "ON"
def format_high_low_inverted(val) when val == 1, do: "LOW" def format_high_low_inverted(val) when val == 1, do: "OFF"
end end

View File

@ -1,8 +0,0 @@
defmodule FarmbotFirmware.Reset do
@moduledoc """
Behaviour to reset the UART connection into
bootloader mode for firmware upgrades.
"""
@callback reset :: :ok | {:error, Stirng.t()}
end

View File

@ -13,7 +13,7 @@ defmodule FarmbotFirmware.UARTTransport do
def init(args) do def init(args) do
device = Keyword.fetch!(args, :device) device = Keyword.fetch!(args, :device)
handle_gcode = Keyword.fetch!(args, :handle_gcode) handle_gcode = Keyword.fetch!(args, :handle_gcode)
reset = Keyword.get(args, :reset) reset = Keyword.fetch!(args, :reset)
{:ok, uart} = UartDefaultAdapter.start_link() {:ok, uart} = UartDefaultAdapter.start_link()
{:ok, {:ok,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -6,11 +6,16 @@ defmodule FarmbotFirmware.CommandTest do
import ExUnit.CaptureLog import ExUnit.CaptureLog
@subject FarmbotFirmware.Command @subject FarmbotFirmware.Command
@tag :capture_log def fake_pid() do
test "command() runs RPCs" do arg = [transport: FarmbotFirmware.StubTransport, reset: StubReset]
arg = [transport: FarmbotFirmware.StubTransport]
{:ok, pid} = FarmbotFirmware.start_link(arg, []) {:ok, pid} = FarmbotFirmware.start_link(arg, [])
send(pid, :timeout) send(pid, :timeout)
pid
end
@tag :capture_log
test "command() runs RPCs" do
pid = fake_pid()
assert {:error, :emergency_lock} == assert {:error, :emergency_lock} ==
FarmbotFirmware.command(pid, {:command_emergency_lock, []}) FarmbotFirmware.command(pid, {:command_emergency_lock, []})
@ -22,9 +27,7 @@ defmodule FarmbotFirmware.CommandTest do
@tag :capture_log @tag :capture_log
test "command() refuses to run RPCs in :boot state" do test "command() refuses to run RPCs in :boot state" do
arg = [transport: FarmbotFirmware.StubTransport] pid = fake_pid()
{:ok, pid} = FarmbotFirmware.start_link(arg, [])
send(pid, :timeout)
{:error, message} = @subject.command(pid, {:a, {:b, :c}}) {:error, message} = @subject.command(pid, {:a, {:b, :c}})
assert "Can't send command when in :boot state" == message assert "Can't send command when in :boot state" == message
end end

View File

@ -14,7 +14,7 @@ defmodule FarmbotFirmware.UARTTransportTest do
init_args = [ init_args = [
device: :FAKE_DEVICE, device: :FAKE_DEVICE,
handle_gcode: :FAKE_GCODE_HANDLER, handle_gcode: :FAKE_GCODE_HANDLER,
reset: :FAKE_RESETER reset: StubReset
] ]
{:ok, state, 0} = UARTTransport.init(init_args) {:ok, state, 0} = UARTTransport.init(init_args)

View File

@ -11,7 +11,7 @@ defmodule FarmbotFirmwareTest do
end end
def firmware_server do def firmware_server do
arg = [transport: FarmbotFirmware.StubTransport] arg = [transport: FarmbotFirmware.StubTransport, reset: StubReset]
{:ok, pid} = FarmbotFirmware.start_link(arg, []) {:ok, pid} = FarmbotFirmware.start_link(arg, [])
send(pid, :timeout) send(pid, :timeout)
try_command(pid, {nil, {:command_emergency_lock, []}}) try_command(pid, {nil, {:command_emergency_lock, []}})

View File

@ -16,19 +16,19 @@ defmodule FarmbotFirmware.ParamTest do
t(:pin_guard_5_time_out, 12, {"pin guard 5 timeout", "(seconds)", "12"}) t(:pin_guard_5_time_out, 12, {"pin guard 5 timeout", "(seconds)", "12"})
t(:pin_guard_5_pin_nr, 12, {"pin guard 5 pin number", nil, "12"}) t(:pin_guard_5_pin_nr, 12, {"pin guard 5 pin number", nil, "12"})
t(:pin_guard_5_active_state, 0, {"pin guard 5 safe state", nil, "HIGH"}) t(:pin_guard_5_active_state, 0, {"pin guard 5 safe state", nil, "ON"})
t(:pin_guard_4_time_out, 12, {"pin guard 4 timeout", "(seconds)", "12"}) t(:pin_guard_4_time_out, 12, {"pin guard 4 timeout", "(seconds)", "12"})
t(:pin_guard_4_pin_nr, 12, {"pin guard 4 pin number", nil, "12"}) t(:pin_guard_4_pin_nr, 12, {"pin guard 4 pin number", nil, "12"})
t(:pin_guard_4_active_state, 0, {"pin guard 4 safe state", nil, "HIGH"}) t(:pin_guard_4_active_state, 0, {"pin guard 4 safe state", nil, "ON"})
t(:pin_guard_3_time_out, 1.0, {"pin guard 3 timeout", "(seconds)", "1"}) t(:pin_guard_3_time_out, 1.0, {"pin guard 3 timeout", "(seconds)", "1"})
t(:pin_guard_3_pin_nr, 1.0, {"pin guard 3 pin number", nil, "1"}) t(:pin_guard_3_pin_nr, 1.0, {"pin guard 3 pin number", nil, "1"})
t(:pin_guard_3_active_state, 0, {"pin guard 3 safe state", nil, "HIGH"}) t(:pin_guard_3_active_state, 0, {"pin guard 3 safe state", nil, "ON"})
t(:pin_guard_2_time_out, 1.0, {"pin guard 2 timeout", "(seconds)", "1"}) t(:pin_guard_2_time_out, 1.0, {"pin guard 2 timeout", "(seconds)", "1"})
t(:pin_guard_2_pin_nr, 1.0, {"pin guard 2 pin number", nil, "1"}) t(:pin_guard_2_pin_nr, 1.0, {"pin guard 2 pin number", nil, "1"})
t(:pin_guard_2_active_state, 0, {"pin guard 2 safe state", nil, "HIGH"}) t(:pin_guard_2_active_state, 0, {"pin guard 2 safe state", nil, "ON"})
t(:pin_guard_1_time_out, 1.0, {"pin guard 1 timeout", "(seconds)", "1"}) t(:pin_guard_1_time_out, 1.0, {"pin guard 1 timeout", "(seconds)", "1"})
t(:pin_guard_1_pin_nr, 1.0, {"pin guard 1 pin number", nil, "1"}) t(:pin_guard_1_pin_nr, 1.0, {"pin guard 1 pin number", nil, "1"})
t(:pin_guard_1_active_state, 0, {"pin guard 1 safe state", nil, "HIGH"}) t(:pin_guard_1_active_state, 0, {"pin guard 1 safe state", nil, "ON"})
t(:param_use_eeprom, 1, {"use eeprom", nil, true}) t(:param_use_eeprom, 1, {"use eeprom", nil, true})
t(:param_test, 1, {"param_test", nil, true}) t(:param_test, 1, {"param_test", nil, true})
t(:param_mov_nr_retry, 1.0, {"max retries", nil, "1"}) t(:param_mov_nr_retry, 1.0, {"max retries", nil, "1"})

View File

@ -80,8 +80,6 @@ config :farmbot, FarmbotOS.Platform.Supervisor,
FarmbotOS.Platform.Host.Configurator FarmbotOS.Platform.Host.Configurator
] ]
config :farmbot_firmware, FarmbotFirmware, reset: FarmbotFirmware.NullReset
config :logger, config :logger,
handle_sasl_reports: false, handle_sasl_reports: false,
handle_otp_reports: false, handle_otp_reports: false,

View File

@ -36,15 +36,7 @@ config :farmbot,
FarmbotCore.Asset.Repo FarmbotCore.Asset.Repo
] ]
config :farmbot_core, FarmbotCore.FirmwareTTYDetector,
expected_names: [
System.get_env("FARMBOT_TTY")
]
config :farmbot_core, FarmbotCore.FirmwareOpenTask, attempt_threshold: 5 config :farmbot_core, FarmbotCore.FirmwareOpenTask, attempt_threshold: 5
config :farmbot_core, FarmbotCore.AssetWorker.FarmbotCore.Asset.FbosConfig,
firmware_flash_attempt_threshold: 5
config :logger, config :logger,
backends: [:console] backends: [:console]

View File

@ -35,8 +35,6 @@ config :farmbot, FarmbotOS.Configurator,
data_layer: FarmbotOS.Configurator.ConfigDataLayer, data_layer: FarmbotOS.Configurator.ConfigDataLayer,
network_layer: FarmbotOS.Configurator.FakeNetworkLayer network_layer: FarmbotOS.Configurator.FakeNetworkLayer
config :farmbot_core, FarmbotCore.FirmwareTTYDetector, expected_names: []
config :farmbot_core, FarmbotCore.FirmwareOpenTask, attempt_threshold: 0 config :farmbot_core, FarmbotCore.FirmwareOpenTask, attempt_threshold: 0
config :farmbot_core, FarmbotCore.AssetWorker.FarmbotCore.Asset.FbosConfig, config :farmbot_core, FarmbotCore.AssetWorker.FarmbotCore.Asset.FbosConfig,

View File

@ -116,9 +116,6 @@ config :farmbot, FarmbotOS.System,
config :farmbot_core, FarmbotCore.FirmwareOpenTask, attempt_threshold: 5 config :farmbot_core, FarmbotCore.FirmwareOpenTask, attempt_threshold: 5
config :farmbot_core, FarmbotCore.AssetWorker.FarmbotCore.Asset.FbosConfig,
firmware_flash_attempt_threshold: 5
config :logger, backends: [RingLogger] config :logger, backends: [RingLogger]
config :logger, RingLogger, config :logger, RingLogger,

View File

@ -1,16 +1,6 @@
use Mix.Config use Mix.Config
config :farmbot_core, FarmbotCore.FirmwareTTYDetector,
expected_names: ["ttyUSB0", "ttyAMA0"]
config :farmbot_firmware, FarmbotFirmware,
reset: FarmbotOS.Platform.Target.FirmwareReset.GPIO
config :farmbot, FarmbotOS.Init.Supervisor, config :farmbot, FarmbotOS.Init.Supervisor,
init_children: [ init_children: [
FarmbotOS.Platform.Target.RTCWorker FarmbotOS.Platform.Target.RTCWorker
] ]
# :farmbot_firmware, FarmbotFirmware changes too much.
# Needed one that would stay stable, so I duplicated it here:
config :farmbot, FarmbotOS.SysCalls.FlashFirmware, gpio: Circuits.GPIO

View File

@ -1,17 +1,7 @@
use Mix.Config use Mix.Config
config :farmbot_core, FarmbotCore.FirmwareTTYDetector,
expected_names: ["ttyUSB0", "ttyAMA0"]
config :farmbot_core, FarmbotCore.FirmwareOpenTask, attempt_threshold: 50 config :farmbot_core, FarmbotCore.FirmwareOpenTask, attempt_threshold: 50
config :farmbot_firmware, FarmbotFirmware,
reset: FarmbotOS.Platform.Target.FirmwareReset.GPIO
# :farmbot_firmware, FarmbotFirmware changes too much.
# Needed one that would stay stable, so I duplicated it here:
config :farmbot, FarmbotOS.SysCalls.FlashFirmware, gpio: Circuits.GPIO
config :farmbot, FarmbotOS.Init.Supervisor, config :farmbot, FarmbotOS.Init.Supervisor,
init_children: [ init_children: [
FarmbotOS.Platform.Target.RTCWorker FarmbotOS.Platform.Target.RTCWorker

View File

@ -1,9 +1,6 @@
use Mix.Config use Mix.Config
config :farmbot_core, FarmbotCore.FirmwareTTYDetector, config :farmbot_firmware, FarmbotFirmware, reset: FarmbotCore.FirmwareResetter
expected_names: ["ttyUSB0", "ttyACM0"]
config :farmbot_firmware, FarmbotFirmware, reset: FarmbotFirmware.NullReset
config :farmbot, FarmbotOS.Init.Supervisor, config :farmbot, FarmbotOS.Init.Supervisor,
init_children: [ init_children: [

View File

@ -84,7 +84,7 @@ defmodule FarmbotOS.SysCalls do
defdelegate set_servo_angle(pin, angle), to: PinControl defdelegate set_servo_angle(pin, angle), to: PinControl
@impl true @impl true
defdelegate resource_update(kind, id, params), to: ResourceUpdate defdelegate update_resource(kind, id, params), to: ResourceUpdate
@impl true @impl true
defdelegate get_current_x(), to: Movement defdelegate get_current_x(), to: Movement
@ -184,6 +184,7 @@ defmodule FarmbotOS.SysCalls do
@impl true @impl true
def firmware_reboot do def firmware_reboot do
FarmbotCore.Logger.info(1, "Restarting firmware...")
GenServer.stop(FarmbotFirmware, :reboot) GenServer.stop(FarmbotFirmware, :reboot)
end end
@ -204,12 +205,14 @@ defmodule FarmbotOS.SysCalls do
@impl true @impl true
def emergency_lock do def emergency_lock do
_ = FarmbotFirmware.command({:command_emergency_lock, []}) _ = FarmbotFirmware.command({:command_emergency_lock, []})
FarmbotCore.Logger.error(1, "E-stopped")
:ok :ok
end end
@impl true @impl true
def emergency_unlock do def emergency_unlock do
_ = FarmbotFirmware.command({:command_emergency_unlock, []}) _ = FarmbotFirmware.command({:command_emergency_unlock, []})
FarmbotCore.Logger.busy(1, "Unlocked")
:ok :ok
end end

View File

@ -1,29 +1,10 @@
Application.get_env(:farmbot, FarmbotOS.SysCalls.FlashFirmware, [])[:gpio]
defmodule FarmbotOS.SysCalls.FlashFirmware do defmodule FarmbotOS.SysCalls.FlashFirmware do
@moduledoc false @moduledoc false
alias FarmbotCore.{Asset, Asset.Private} alias FarmbotCore.{Asset, Asset.Private, FirmwareResetter}
alias FarmbotFirmware alias FarmbotFirmware
alias FarmbotCore.FirmwareTTYDetector alias FarmbotCore.FirmwareTTYDetector
defmodule Stub do
require FarmbotCore.Logger
def fail do
m = "No express function found. Please notify support."
FarmbotCore.Logger.error(3, m)
{:error, m}
end
def open(_, _), do: fail()
def write(_, _), do: fail()
end
# This only matter for express.
# When it's an express, use Circuits.GPIO.
@gpio Application.get_env(:farmbot, __MODULE__, [])[:gpio] || Stub
import FarmbotFirmware.PackageUtils, import FarmbotFirmware.PackageUtils,
only: [find_hex_file: 1, package_to_string: 1] only: [find_hex_file: 1, package_to_string: 1]
@ -40,11 +21,11 @@ defmodule FarmbotOS.SysCalls.FlashFirmware do
{:ok, tty} <- find_tty(), {:ok, tty} <- find_tty(),
_ <- _ <-
FarmbotCore.Logger.debug(3, "found tty: #{tty} for firmware flash"), FarmbotCore.Logger.debug(3, "found tty: #{tty} for firmware flash"),
{:ok, fun} <- find_reset_fun(package), {:ok, fun} <- FirmwareResetter.find_reset_fun(package),
_ <- _ <-
FarmbotCore.Logger.debug( FarmbotCore.Logger.debug(
3, 3,
"closing firmware transport before flash" "Closing the firmware transport before flash"
), ),
:ok <- FarmbotFirmware.close_transport(), :ok <- FarmbotFirmware.close_transport(),
_ <- FarmbotCore.Logger.debug(3, "starting firmware flash"), _ <- FarmbotCore.Logger.debug(3, "starting firmware flash"),
@ -64,14 +45,17 @@ defmodule FarmbotOS.SysCalls.FlashFirmware do
end end
def finish_flashing({_result, 0}) do def finish_flashing({_result, 0}) do
FarmbotCore.Logger.success(2, "Firmware flashed successfully!") FarmbotCore.Logger.success(
1,
"Firmware flashed successfully. Unlock FarmBot to finish initialization."
)
end end
def finish_flashing(result) do def finish_flashing(result) do
FarmbotCore.Logger.debug(2, "AVR flash returned #{inspect(result)}") FarmbotCore.Logger.debug(2, "AVR flash returned #{inspect(result)}")
end end
defp find_tty() do def find_tty() do
case FirmwareTTYDetector.tty() do case FirmwareTTYDetector.tty() do
nil -> nil ->
{:error, {:error,
@ -83,32 +67,4 @@ defmodule FarmbotOS.SysCalls.FlashFirmware do
{:ok, tty} {:ok, tty}
end end
end end
defp find_reset_fun("express_k10") do
FarmbotCore.Logger.debug(3, "Using special express reset function")
{:ok, fn -> express_reset_fun() end}
end
defp find_reset_fun(_) do
FarmbotCore.Logger.debug(3, "Using default reset function")
{:ok, &FarmbotFirmware.NullReset.reset/0}
end
def express_reset_fun() do
try do
FarmbotCore.Logger.debug(3, "Begin MCU reset")
{:ok, gpio} = @gpio.open(19, :output)
:ok = @gpio.write(gpio, 0)
:ok = @gpio.write(gpio, 1)
Process.sleep(1000)
:ok = @gpio.write(gpio, 0)
FarmbotCore.Logger.debug(3, "Finish MCU Reset")
:ok
rescue
ex ->
message = Exception.message(ex)
Logger.error("Could not flash express firmware: #{message}")
:express_reset_error
end
end
end end

View File

@ -98,6 +98,12 @@ defmodule FarmbotOS.SysCalls.Movement do
# TODO(Rick): Figure out source of Error: {:ok, "ok"} logs. # TODO(Rick): Figure out source of Error: {:ok, "ok"} logs.
def handle_movement_error({:ok, _}), do: :ok def handle_movement_error({:ok, _}), do: :ok
def handle_movement_error(:emergency_lock) do
msg = "Cannot execute commands while E-stopped"
FarmbotCore.Logger.busy(1, msg)
{:error, msg}
end
def handle_movement_error(reason) do def handle_movement_error(reason) do
msg = "Movement failed. #{inspect(reason)}" msg = "Movement failed. #{inspect(reason)}"
FarmbotCore.Logger.error(1, msg) FarmbotCore.Logger.error(1, msg)

View File

@ -8,8 +8,21 @@ defmodule FarmbotOS.SysCalls.PointLookup do
def point(kind, id) do def point(kind, id) do
case Asset.get_point(id: id) do case Asset.get_point(id: id) do
nil -> {:error, "#{kind} not found"} nil ->
%{name: name, x: x, y: y, z: z} -> %{name: name, x: x, y: y, z: z} {:error, "#{kind || "point"} #{id} not found"}
%{name: name, x: x, y: y, z: z, pointer_type: type} ->
%{
name: name,
resource_type: type,
resource_id: id,
x: x,
y: y,
z: z
}
other ->
Logger.debug("Point error: Please notify support #{inspect(other)}")
end end
end end

View File

@ -2,6 +2,7 @@ defmodule FarmbotOS.SysCalls.ResourceUpdate do
@moduledoc false @moduledoc false
require Logger require Logger
require FarmbotCore.Logger
alias FarmbotCore.{ alias FarmbotCore.{
Asset, Asset,
@ -10,9 +11,39 @@ defmodule FarmbotOS.SysCalls.ResourceUpdate do
alias FarmbotOS.SysCalls.SendMessage alias FarmbotOS.SysCalls.SendMessage
@point_kinds ~w(Plant GenericPointer) @point_kinds ~w(Plant GenericPointer ToolSlot Weed)
@friendly_names %{
"gantry_mounted" => "`gantry mounted` property",
"mounted_tool_id" => "mounted tool ID",
"openfarm_slug" => "Openfarm slug",
"ota_hour" => "OTA hour",
"plant_stage" => "plant stage",
"planted_at" => "planted at time",
"pullout_direction" => "pullout direction",
"tool_id" => "tool ID",
"tz_offset_hrs" => "timezone offset hours",
"x" => "X axis",
"y" => "Y axis",
"z" => "Z axis",
"Device" => "device",
"Plant" => "plant",
"GenericPointer" => "map point",
"ToolSlot" => "tool slot",
"Weed" => "weed"
}
def notify_user_of_updates(kind, params, id \\ nil) do
Enum.map(params, fn {k, v} ->
name = @friendly_names[kind] || kind
property = @friendly_names["#{k}"] || k
msg = "Setting #{name} #{id} #{property} to #{inspect(v)}"
FarmbotCore.Logger.info(3, msg)
end)
end
def update_resource("Device" = kind, _, params) do
notify_user_of_updates(kind, params)
def resource_update("Device", 0, params) do
params params
|> do_handlebars() |> do_handlebars()
|> Asset.update_device!() |> Asset.update_device!()
@ -21,12 +52,13 @@ defmodule FarmbotOS.SysCalls.ResourceUpdate do
:ok :ok
end end
def resource_update(kind, id, params) when kind in @point_kinds do def update_resource(kind, id, params) when kind in @point_kinds do
notify_user_of_updates(kind, params, id)
params = do_handlebars(params) params = do_handlebars(params)
point_resource_update(kind, id, params) point_update_resource(kind, id, params)
end end
def resource_update(kind, id, _params) do def update_resource(kind, id, _params) do
{:error, {:error,
""" """
Unknown resource: #{kind}.#{id} Unknown resource: #{kind}.#{id}
@ -34,18 +66,26 @@ defmodule FarmbotOS.SysCalls.ResourceUpdate do
end end
@doc false @doc false
def point_resource_update(type, id, params) do def point_update_resource(type, id, params) do
with %{} = point <- Asset.get_point(pointer_type: type, id: id), with %{} = point <- Asset.get_point(id: id),
{:ok, point} <- Asset.update_point(point, params) do {:ok, point} <- Asset.update_point(point, params) do
_ = Private.mark_dirty!(point) _ = Private.mark_dirty!(point)
:ok :ok
else else
nil -> nil ->
{:error, msg = "#{type}.#{id} is not currently synced. Please re-sync."
"#{type}.#{id} is not currently synced, so it could not be updated"} FarmbotCore.Logger.error(3, msg)
{:error, msg}
{:error, _changeset} -> {:error, _changeset} ->
{:error, "Failed to update #{type}.#{id}"} msg =
"Failed update (#{type}.#{id}): Ensure the data is properly formatted"
FarmbotCore.Logger.error(3, msg)
{:error, msg}
err ->
{:error, "Unknown error. Please notify support. #{inspect(err)}"}
end end
end end
@ -59,7 +99,7 @@ defmodule FarmbotOS.SysCalls.ResourceUpdate do
_ -> _ ->
Logger.warn( Logger.warn(
"failed to render #{key} => #{value} for resource_update" "failed to render #{key} => #{value} for update_resource"
) )
{key, value} {key, value}

View File

@ -1,34 +0,0 @@
defmodule FarmbotOS.Platform.Target.FirmwareReset.GPIO do
@moduledoc """
Uses GPIO pin 19 to reset the firmware.
"""
@behaviour FarmbotFirmware.Reset
use GenServer
require Logger
@impl FarmbotFirmware.Reset
def reset(server \\ __MODULE__) do
Logger.debug("calling gpio reset/0")
GenServer.call(server, :reset)
end
@impl GenServer
def init(_args) do
Logger.debug("initializing gpio thing for firmware reset")
{:ok, gpio} = Circuits.GPIO.open(19, :output)
{:ok, %{gpio: gpio}}
end
@impl GenServer
def handle_call(:reset, _from, state) do
Logger.warn("doing firmware gpio reset")
with :ok <- Circuits.GPIO.write(state.gpio, 1),
:ok <- Circuits.GPIO.write(state.gpio, 0) do
{:reply, :ok, state}
else
error -> {:reply, error, state}
end
end
end

View File

@ -542,7 +542,7 @@ defmodule FarmbotOS.Platform.Target.NervesHubClient do
"ota_hour = #{ota_hour || "null"} timezone = #{timezone || "null"}" "ota_hour = #{ota_hour || "null"} timezone = #{timezone || "null"}"
) )
true !!auto_update
end end
result && !currently_downloading?() result && !currently_downloading?()

View File

@ -17,7 +17,7 @@ defmodule FarmbotOS.SysCalls.PointLookupTest do
test "failure cases" do test "failure cases" do
err1 = PointLookup.point("GenericPointer", 24) err1 = PointLookup.point("GenericPointer", 24)
assert {:error, "GenericPointer not found"} == err1 assert {:error, "GenericPointer 24 not found"} == err1
err2 = PointLookup.get_toolslot_for_tool(24) err2 = PointLookup.get_toolslot_for_tool(24)
assert {:error, "Could not find point for tool by id: 24"} == err2 assert {:error, "Could not find point for tool by id: 24"} == err2
@ -33,7 +33,9 @@ defmodule FarmbotOS.SysCalls.PointLookupTest do
name: "test suite III", name: "test suite III",
x: 1.2, x: 1.2,
y: 3.4, y: 3.4,
z: 5.6 z: 5.6,
resource_id: 555,
resource_type: "GenericPointer"
} }
p = point(expected) p = point(expected)

View File

@ -14,14 +14,14 @@ defmodule FarmbotOS.SysCalls.ResourceUpdateTest do
end) end)
end end
test "resource_update/3 - Device" do test "update_resource/3 - Device" do
fake_coords!() fake_coords!()
params = %{name: "X is {{ x }}"} params = %{name: "X is {{ x }}"}
assert :ok == ResourceUpdate.resource_update("Device", 0, params) assert :ok == ResourceUpdate.update_resource("Device", 0, params)
assert "X is 1.2" == FarmbotCore.Asset.device().name assert "X is 1.2" == FarmbotCore.Asset.device().name
end end
test "resource_update/3 - Point" do test "update_resource/3 - Point" do
Repo.delete_all(Point) Repo.delete_all(Point)
%Point{id: 555, pointer_type: "Plant"} %Point{id: 555, pointer_type: "Plant"}
@ -29,17 +29,17 @@ defmodule FarmbotOS.SysCalls.ResourceUpdateTest do
|> Repo.insert!() |> Repo.insert!()
params = %{name: "Updated to {{ x }}"} params = %{name: "Updated to {{ x }}"}
assert :ok == ResourceUpdate.resource_update("Plant", 555, params) assert :ok == ResourceUpdate.update_resource("Plant", 555, params)
next_plant = PointLookup.point("Plant", 555) next_plant = PointLookup.point("Plant", 555)
assert "Updated to " == next_plant.name assert String.contains?(next_plant.name, "Updated to ")
bad_result1 = ResourceUpdate.resource_update("Plant", 0, params) bad_result1 = ResourceUpdate.update_resource("Plant", 0, params)
error = "Plant.0 is not currently synced, so it could not be updated" error = "Plant.0 is not currently synced. Please re-sync."
assert {:error, error} == bad_result1 assert {:error, error} == bad_result1
end end
test "resource_update/3 - unknown" do test "update_resource/3 - unknown" do
{:error, error} = ResourceUpdate.resource_update("Foo", 0, nil) {:error, error} = ResourceUpdate.update_resource("Foo", 0, nil)
assert error == "Unknown resource: Foo.0\n" assert error == "Unknown resource: Foo.0\n"
end end
end end