From 209ded96cbbd2ea16ae8490707d316016c5ff032 Mon Sep 17 00:00:00 2001 From: Connor Rigby Date: Fri, 23 Aug 2019 10:31:13 -0700 Subject: [PATCH] Add Lua helpers for updating and getting resources --- docs/celery_script/assert expressions.md | 106 ++++++++++++++++-- .../lib/farmbot_celery_script/compiler.ex | 30 +++-- .../lib/farmbot_celery_script/sys_calls.ex | 17 +-- .../farmbot_celery_script/sys_calls/stubs.ex | 2 +- .../lib/farmbot_ext/amqp/log_channel.ex | 5 +- farmbot_os/lib/farmbot_os/lua.ex | 30 ++++- farmbot_os/lib/farmbot_os/lua/ext/data.ex | 73 ++++++++++++ farmbot_os/lib/farmbot_os/lua/ext/info.ex | 93 +++++---------- farmbot_os/lib/farmbot_os/lua/util.ex | 12 ++ farmbot_os/lib/farmbot_os/sys_calls.ex | 4 +- test/support/celery_script/test_sys_calls.ex | 4 +- 11 files changed, 277 insertions(+), 99 deletions(-) create mode 100644 farmbot_os/lib/farmbot_os/lua/ext/data.ex create mode 100644 farmbot_os/lib/farmbot_os/lua/util.ex diff --git a/docs/celery_script/assert expressions.md b/docs/celery_script/assert expressions.md index a681f5ef..53310f76 100644 --- a/docs/celery_script/assert expressions.md +++ b/docs/celery_script/assert expressions.md @@ -11,12 +11,6 @@ standard library](https://www.lua.org/manual/5.2/). ```lua -- Comments are ignored by the interpreter --- help(function_name) --- Returns docs for a function - -print(help("send_message")); -print(help("get_position")); - -- get_position() -- Returns a table containing the current position data @@ -28,6 +22,8 @@ else return false; end +return get_position("y") <= 20; + -- get_pins() -- Returns a table containing current pin data @@ -36,10 +32,106 @@ if pins[9] == 1.0 then return true; end +return get_pin(10) == 0; + -- send_message(type, message, channels) -- Sends a message to farmbot's logger -send_message("info", "hello, world", ["toast"]) +send_message("info", "hello, world", ["toast"]); + +-- calibrate(axis) +-- calibrate an axis + +calibrate("x"); +calibrate("y"); +calibrate("z"); + +-- emergency_lock() +-- emergency_unlock() +-- lock and unlock farmbot's firmware. +send_message("error", "locking the firmware!"); +emergancy_lock(); +emergency_unlock(); + +-- find_home(axis) +-- Find home on an axis. + +find_home("x"); +find_home("y"); +find_home("z"); + +-- home(axis) +-- Go to home on an axis. + +home("x"); +home("y"); +home("z"); + +-- coordinate(x, y, z) +-- create a vec3 +move_to = coordinate(1.0, 0, 0); + +-- move_absolute(x, y, z) +-- Move in a line to a position +move_absolute(1.0, 0, 0); +move_absolute(coordinate(1.0, 20, 30)); + +-- check_position(vec3, tolerance) +-- Check a position against Farmbot's current +-- position within a error threshold + +move_absolute(1.0, 0, 0); +return check_position({x = 1.0, y = 0; z = 0}, 0.50); + +move_absolute(20, 100, 100); +return check_position(coordinate(20, 100, 100), 1); + +-- read_status(arg0) +-- Get a field on farmbot's current state + +status = read_status(); +return status.informational_settings.wifi_level >= 5; + +return read_status("location_data", "raw_encoders") >= 1900; + +-- version() +-- return Farmbot's current version +return version() == "8.1.2"; + +-- get_device(field) +-- return the device settings + +return get_device().timezone == "America/los_angeles"; + +return get_device("name") == "Test Farmbot"; + +-- update_device(table) +-- update device settings + +update_device({name = "Test Farmbot"}); + +-- get_fbos_config(field) +-- return the current fbos_config + +return get_fbos_config("auto_sync"); +return get_fbos_config().os_auto_update; + +-- update_fbos_config(table) +-- update the current fbos_config + +update_fbos_config({auto_sync = true, os_auto_update = false}); + +-- get_firmware_config(field) +-- return current firmware_config data + +return get_firmware_config().encoder_enabled_x == 1.0; + +return get_firmware_config("encoder_enabled_z"); + +-- update_firmware_config(table) +-- update current firmware_config data + +update_firmware_config({encoder_enabled_z = 1.0}); ``` ## Expression contract diff --git a/farmbot_celery_script/lib/farmbot_celery_script/compiler.ex b/farmbot_celery_script/lib/farmbot_celery_script/compiler.ex index 3544bba7..01b12254 100644 --- a/farmbot_celery_script/lib/farmbot_celery_script/compiler.ex +++ b/farmbot_celery_script/lib/farmbot_celery_script/compiler.ex @@ -187,11 +187,16 @@ defmodule FarmbotCeleryScript.Compiler do quote location: :keep do comment_header = unquote(comment_header) + assertion_type = unquote(assertion_type) - case FarmbotCeleryScript.SysCalls.eval_assertion(unquote(compile_ast(expression))) do + case FarmbotCeleryScript.SysCalls.eval_assertion( + unquote(comment), + unquote(compile_ast(expression)) + ) do {:error, reason} -> FarmbotCeleryScript.SysCalls.log_assertion( false, + assertion_type, "#{comment_header}failed to evaluate, aborting" ) @@ -200,34 +205,43 @@ defmodule FarmbotCeleryScript.Compiler do true -> FarmbotCeleryScript.SysCalls.log_assertion( true, + assertion_type, "#{comment_header}passed, continuing execution" ) :ok - false when unquote(assertion_type) == "continue" -> + false when assertion_type == "continue" -> FarmbotCeleryScript.SysCalls.log_assertion( false, + assertion_type, "#{comment_header}failed, continuing execution" ) :ok - false when unquote(assertion_type) == "abort" -> - FarmbotCeleryScript.SysCalls.log_assertion(false, "#{comment_header}failed, aborting") - {:error, "Assertion failed (aborting)"} - - false when unquote(assertion_type) == "recover" -> + false when assertion_type == "abort" -> FarmbotCeleryScript.SysCalls.log_assertion( false, + assertion_type, + "#{comment_header}failed, aborting" + ) + + {:error, "Assertion failed (aborting)"} + + false when assertion_type == "recover" -> + FarmbotCeleryScript.SysCalls.log_assertion( + false, + assertion_type, "#{comment_header}failed, recovering and continuing" ) unquote(compile_block(then_ast)) - false when unquote(assertion_type) == "abort_recover" -> + false when assertion_type == "abort_recover" -> FarmbotCeleryScript.SysCalls.log_assertion( false, + assertion_type, "#{comment_header}failed, recovering and aborting" ) diff --git a/farmbot_celery_script/lib/farmbot_celery_script/sys_calls.ex b/farmbot_celery_script/lib/farmbot_celery_script/sys_calls.ex index e5f816fe..a13c078a 100644 --- a/farmbot_celery_script/lib/farmbot_celery_script/sys_calls.ex +++ b/farmbot_celery_script/lib/farmbot_celery_script/sys_calls.ex @@ -67,24 +67,25 @@ defmodule FarmbotCeleryScript.SysCalls do @callback log(message :: String.t()) :: any() @callback sequence_init_log(message :: String.t()) :: any() @callback sequence_complete_log(message :: String.t()) :: any() - @callback eval_assertion(expression :: String.t()) :: true | false | error() + @callback eval_assertion(comment :: String.t(), expression :: String.t()) :: + true | false | error() - def eval_assertion(sys_calls \\ @sys_calls, expression) when is_binary(expression) do - case sys_calls.eval_assertion(expression) do + def eval_assertion(sys_calls \\ @sys_calls, comment, expression) when is_binary(expression) do + case sys_calls.eval_assertion(comment, expression) do true -> true false -> false - {:error, reason} when is_binary(reason) -> - or_error(sys_calls, :eval_assertion, [expression], reason) + {:error, reason} = error when is_binary(reason) -> + or_error(sys_calls, :eval_assertion, [comment, expression], error) end end - def log_assertion(sys_calls \\ @sys_calls, passed?, message) do - if function_exported?(sys_calls, :log_assertion, 2) do - apply(sys_calls, :log_assertion, [passed?, message]) + def log_assertion(sys_calls \\ @sys_calls, passed?, type, message) do + if function_exported?(sys_calls, :log_assertion, 3) do + apply(sys_calls, :log_assertion, [passed?, type, message]) end end diff --git a/farmbot_celery_script/lib/farmbot_celery_script/sys_calls/stubs.ex b/farmbot_celery_script/lib/farmbot_celery_script/sys_calls/stubs.ex index 4af7d348..4017b852 100644 --- a/farmbot_celery_script/lib/farmbot_celery_script/sys_calls/stubs.ex +++ b/farmbot_celery_script/lib/farmbot_celery_script/sys_calls/stubs.ex @@ -130,7 +130,7 @@ defmodule FarmbotCeleryScript.SysCalls.Stubs do def zero(axis), do: error(:zero, [axis]) @impl true - def eval_assertion(expression), do: error(:eval_assertion, [expression]) + def eval_assertion(comment, expression), do: error(:eval_assertion, [comment, expression]) defp error(fun, _args) do msg = """ diff --git a/farmbot_ext/lib/farmbot_ext/amqp/log_channel.ex b/farmbot_ext/lib/farmbot_ext/amqp/log_channel.ex index 14993f4e..210047b7 100644 --- a/farmbot_ext/lib/farmbot_ext/amqp/log_channel.ex +++ b/farmbot_ext/lib/farmbot_ext/amqp/log_channel.ex @@ -96,7 +96,10 @@ defmodule FarmbotExt.AMQP.LogChannel do # ANSWER(Connor) - because the FE needed it. created_at: DateTime.from_naive!(log.inserted_at, "Etc/UTC") |> DateTime.to_unix(), channels: log.meta[:channels] || log.meta["channels"] || [], - assertion_passed: log.meta[:assertion_passed], + meta: %{ + assertion_passed: log.meta[:assertion_passed], + assertion_type: log.meta[:assertion_type] + }, message: log.message } diff --git a/farmbot_os/lib/farmbot_os/lua.ex b/farmbot_os/lib/farmbot_os/lua.ex index 2137e966..625872fb 100644 --- a/farmbot_os/lib/farmbot_os/lua.ex +++ b/farmbot_os/lib/farmbot_os/lua.ex @@ -1,21 +1,24 @@ defmodule FarmbotOS.Lua do @type t() :: tuple() @type table() :: [{any, any}] + require FarmbotCore.Logger + alias FarmbotOS.Lua.Ext.{ + Data, Firmware, Info } - def log_assertion(passed?, message) do - # FarmbotCore.Logger.dispatch_log(__ENV__, :assertion, 2, message, [assertion_passed: passed?]) - FarmbotCore.Logger.dispatch_log(__ENV__, :info, 2, message, assertion_passed: passed?) + def log_assertion(passed?, type, message) do + meta = [assertion_passed: passed?, assertion_type: type] + FarmbotCore.Logger.dispatch_log(__ENV__, :assertion, 2, message, meta) end @doc """ Evaluates some Lua code. The code should return a boolean value. """ - def eval_assertion(str) when is_binary(str) do + def eval_assertion(comment, str) when is_binary(str) do init() |> eval(str) |> case do @@ -35,6 +38,17 @@ defmodule FarmbotOS.Lua do {:error, "lua runtime error evaluating expression"} {:error, {:badmatch, {:error, [{line, :luerl_parse, parse_error}], _}}} -> + FarmbotCore.Logger.error( + 1, + """ + Failed to parse expression: + `#{comment}.lua:#{line}` + + #{IO.iodata_to_binary(parse_error)} + """, + channels: [:toast] + ) + {:error, "failed to parse expression (line:#{line}): #{IO.iodata_to_binary(parse_error)}"} error -> @@ -59,6 +73,14 @@ defmodule FarmbotOS.Lua do |> set_table([:read_status], &Info.read_status/2) |> set_table([:send_message], &Info.send_message/2) |> set_table([:version], &Info.version/2) + |> set_table([:update_device], &Data.update_device/2) + |> set_table([:get_device], &Data.get_device/2) + |> set_table([:update_fbos_config], &Data.update_fbos_config/2) + |> set_table([:get_fbos_config], &Data.get_fbos_config/2) + |> set_table([:update_firmware_config], &Data.update_firmware_config/2) + |> set_table([:get_firmware_config], &Data.get_firmware_config/2) + |> set_table([:new_farmware_env], &Data.new_farmware_env/2) + |> set_table([:new_sensor_reading], &Data.new_sensor_reading/2) end @spec set_table(t(), Path.t(), any()) :: t() diff --git a/farmbot_os/lib/farmbot_os/lua/ext/data.ex b/farmbot_os/lib/farmbot_os/lua/ext/data.ex new file mode 100644 index 00000000..480873c4 --- /dev/null +++ b/farmbot_os/lib/farmbot_os/lua/ext/data.ex @@ -0,0 +1,73 @@ +defmodule FarmbotOS.Lua.Ext.Data do + @moduledoc """ + Extensions for manipulating data from Lua + """ + import FarmbotOS.Lua.Util + + alias FarmbotCore.{ + Asset, + Asset.Device, + Asset.FbosConfig, + Asset.FirmwareConfig + } + + def update_device([table], lua) do + params = Map.new(table) + _ = Asset.update_device!(params) + {[true], lua} + end + + def get_device([field], lua) do + device = Asset.device() |> Device.render() + {[device[String.to_atom(field)]], lua} + end + + def get_device(_, lua) do + device = Asset.device() |> Device.render() + {[map_to_table(device)], lua} + end + + def update_fbos_config([table], lua) do + params = Map.new(table) + _ = Asset.update_fbos_config!(params) + {[true], lua} + end + + def get_fbos_config([field], lua) do + fbos_config = Asset.fbos_config() |> FbosConfig.render() + {[fbos_config[String.to_atom(field)]], lua} + end + + def get_fbos_config(_, lua) do + fbos_config = Asset.fbos_config() |> FbosConfig.render() + {[map_to_table(fbos_config)], lua} + end + + def update_firmware_config([table], lua) do + params = Map.new(table) + _ = Asset.update_firmware_config!(params) + {[true], lua} + end + + def get_firmware_config([field], lua) do + firmware_config = Asset.firmware_config() |> FirmwareConfig.render() + {[firmware_config[String.to_atom(field)]], lua} + end + + def get_firmware_config(_, lua) do + firmware_config = Asset.firmware_config() |> FirmwareConfig.render() + {[map_to_table(firmware_config)], lua} + end + + def new_farmware_env([table], lua) do + params = Map.new(table) + _ = Asset.new_farmware_env(params) + {[true], lua} + end + + def new_sensor_reading([table], lua) do + params = Map.new(table) + _ = Asset.new_sensor_reading!(params) + {[true], lua} + end +end diff --git a/farmbot_os/lib/farmbot_os/lua/ext/info.ex b/farmbot_os/lib/farmbot_os/lua/ext/info.ex index 28425815..970d261a 100644 --- a/farmbot_os/lib/farmbot_os/lua/ext/info.ex +++ b/farmbot_os/lib/farmbot_os/lua/ext/info.ex @@ -1,4 +1,7 @@ defmodule FarmbotOS.Lua.Ext.Info do + @moduledoc """ + Lua extensions for gathering information about Farmbot + """ alias FarmbotCeleryScript.SysCalls @doc """ @@ -22,76 +25,24 @@ defmodule FarmbotOS.Lua.Ext.Info do do_send_message(kind, message, channels, lua) end - def read_status(_, lua) do - bot_state = FarmbotCore.BotState.fetch() + @doc "Returns data about the bot's state" + def read_status([], lua) do + bot_state = FarmbotCore.BotState.fetch() |> FarmbotCore.BotStateNG.view() - configuration_table = [ - {"arduino_debug_messages", bot_state.configuration.arduino_debug_messages}, - {"auto_sync", bot_state.configuration.auto_sync}, - {"beta_opt_in", bot_state.configuration.beta_opt_in}, - {"disable_factory_reset", bot_state.configuration.disable_factory_reset}, - {"firmware_hardware", bot_state.configuration.firmware_hardware}, - {"firmware_input_log", bot_state.configuration.firmware_input_log}, - {"firmware_output_log", bot_state.configuration.firmware_output_log}, - {"network_not_found_timer", bot_state.configuration.network_not_found_timer}, - {"os_auto_update", bot_state.configuration.os_auto_update}, - {"sequence_body_log", bot_state.configuration.sequence_body_log}, - {"sequence_complete_log", bot_state.configuration.sequence_complete_log}, - {"sequence_init_log", bot_state.configuration.sequence_init_log} - ] + {[map_to_table(bot_state)], lua} + end - informational_settings_table = [ - {"busy", bot_state.informational_settings.busy}, - {"cache_bust", bot_state.informational_settings.cache_bust}, - {"commit", bot_state.informational_settings.commit}, - {"controller_version", bot_state.informational_settings.controller_version}, - {"disk_usage", bot_state.informational_settings.disk_usage}, - {"env", bot_state.informational_settings.env}, - {"firmware_commit", bot_state.informational_settings.firmware_commit}, - {"firmware_version", bot_state.informational_settings.firmware_version}, - {"idle", bot_state.informational_settings.idle}, - {"last_status", bot_state.informational_settings.last_status}, - {"locked", bot_state.informational_settings.locked}, - {"memory_usage", bot_state.informational_settings.memory_usage}, - {"node_name", bot_state.informational_settings.node_name}, - {"soc_temp", bot_state.informational_settings.soc_temp}, - {"sync_status", bot_state.informational_settings.sync_status}, - {"target", bot_state.informational_settings.target}, - {"throttled", bot_state.informational_settings.throttled}, - {"update_available", bot_state.informational_settings.update_available}, - {"uptime", bot_state.informational_settings.uptime}, - {"wifi_level", bot_state.informational_settings.wifi_level}, - {"wifi_level_percent", bot_state.informational_settings.wifi_level_percent} - ] + def read_status(path, lua) do + bot_state = FarmbotCore.BotState.fetch() |> FarmbotCore.BotStateNG.view() + path = List.flatten(path) |> Enum.map(&String.to_atom(&1)) - location_data_table = [ - {"position", - [ - {"x", bot_state.location_data.position.x}, - {"y", bot_state.location_data.position.y}, - {"z", bot_state.location_data.position.z} - ]}, - {"raw_encoders", - [ - {"x", bot_state.location_data.raw_encoders.x}, - {"y", bot_state.location_data.raw_encoders.y}, - {"z", bot_state.location_data.raw_encoders.z} - ]}, - {"scaled_encoders", - [ - {"x", bot_state.location_data.scaled_encoders.x}, - {"y", bot_state.location_data.scaled_encoders.y}, - {"z", bot_state.location_data.scaled_encoders.z} - ]} - ] + case get_in(bot_state, path) do + %{} = map -> + {[map_to_table(map)], lua} - final_table = [ - {"configuration", configuration_table}, - {"informational_settings", informational_settings_table}, - {"location_data", location_data_table} - ] - - {[final_table, nil], lua} + other -> + {[other], lua} + end end @doc "Returns the current version of farmbot." @@ -108,4 +59,14 @@ defmodule FarmbotOS.Lua.Ext.Info do {[nil, reason], lua} end end + + defp map_to_table(map) do + Enum.map(map, fn + {key, %{} = value} -> + {to_string(key), map_to_table(value)} + + {key, value} -> + {to_string(key), value} + end) + end end diff --git a/farmbot_os/lib/farmbot_os/lua/util.ex b/farmbot_os/lib/farmbot_os/lua/util.ex new file mode 100644 index 00000000..cbc7fd79 --- /dev/null +++ b/farmbot_os/lib/farmbot_os/lua/util.ex @@ -0,0 +1,12 @@ +defmodule FarmbotOS.Lua.Util do + @doc "Convert an Elixir map to a Lua table" + def map_to_table(map) do + Enum.map(map, fn + {key, %{} = value} -> + {to_string(key), map_to_table(value)} + + {key, value} -> + {to_string(key), value} + end) + end +end diff --git a/farmbot_os/lib/farmbot_os/sys_calls.ex b/farmbot_os/lib/farmbot_os/sys_calls.ex index 93505138..8f504afa 100644 --- a/farmbot_os/lib/farmbot_os/sys_calls.ex +++ b/farmbot_os/lib/farmbot_os/sys_calls.ex @@ -61,8 +61,8 @@ defmodule FarmbotOS.SysCalls do defdelegate set_pin_io_mode(pin, mode), to: SetPinIOMode @impl true - defdelegate eval_assertion(expression), to: Lua - defdelegate log_assertion(passed?, message), to: Lua + defdelegate eval_assertion(comment, expression), to: Lua + defdelegate log_assertion(passed?, type, message), to: Lua @impl true def log(message) do diff --git a/test/support/celery_script/test_sys_calls.ex b/test/support/celery_script/test_sys_calls.ex index c01497f1..17248640 100644 --- a/test/support/celery_script/test_sys_calls.ex +++ b/test/support/celery_script/test_sys_calls.ex @@ -242,8 +242,8 @@ defmodule Farmbot.TestSupport.CeleryScript.TestSysCalls do end @impl true - def eval_assertion(expression) do - call({:eval_assertion, [expression]}) + def eval_assertion(comment, expression) do + call({:eval_assertion, [comment, expression]}) end defp call(data) do