defmodule Farmbot.CeleryScript.RunTime.InstructionSet do @moduledoc """ Implementation for each and every executable CeleryScript AST node. """ alias Farmbot.CeleryScript.RunTime.{ FarmProc, Instruction, SysCallHandler, Resolver } alias Farmbot.CeleryScript.AST import Farmbot.CeleryScript.Utils import Instruction, only: [simple_io_instruction: 1] import SysCallHandler, only: [apply_sys_call_fun: 2] import FarmProc, only: [ get_pc_ptr: 1, get_next_address: 2, get_body_address: 2, get_cell_attr_as_pointer: 3, pop_rs: 1, push_rs: 2, set_pc_ptr: 2, set_status: 2, set_crash_reason: 2, clear_io_result: 1, set_io_latch: 2, get_heap_by_page_index: 2, new_page: 3, get_zero_page: 1, is_null_address?: 1 ] # Command Nodes @doc "Write a pin." simple_io_instruction(:write_pin) @doc "Read a pin." simple_io_instruction(:read_pin) @doc "Write servo pin value." simple_io_instruction(:set_servo_angle) @doc "Send a message." simple_io_instruction(:send_message) @doc "move relative to the bot's current position." simple_io_instruction(:move_relative) @doc "Move an axis home." simple_io_instruction(:home) @doc "Find an axis home." simple_io_instruction(:find_home) @doc "Wait (block) a number of milliseconds." simple_io_instruction(:wait) @doc "Toggle a pin atomicly." simple_io_instruction(:toggle_pin) @doc "Execute a Farmware." simple_io_instruction(:execute_script) @doc "Force axis position to become zero." simple_io_instruction(:zero) @doc "Calibrate an axis." simple_io_instruction(:calibrate) @doc "Execute `take_photo` Farmware if installed." simple_io_instruction(:take_photo) # RPC Nodes @doc "Update bot or firmware configuration." simple_io_instruction(:config_update) @doc "Set environment variables for a Farmware." simple_io_instruction(:set_user_env) @doc "(Re)Install Farmware written and developed by Farmbot, Inc." simple_io_instruction(:install_first_party_farmware) @doc "Install a Farmware from the web." simple_io_instruction(:install_farmware) @doc "Remove a Farmware." simple_io_instruction(:uninstall_farmware) @doc "Update a Farmware." simple_io_instruction(:update_farmware) @doc "Force the bot's state to be dispatched." simple_io_instruction(:read_status) @doc "Sync all resources with the Farmbot Web Application." simple_io_instruction(:sync) @doc "Power the bot down." simple_io_instruction(:power_off) @doc "Reboot the bot." simple_io_instruction(:reboot) @doc "Factory reset the bot allowing for reconfiguration." simple_io_instruction(:factory_reset) @doc "Factory reset the bot, but supply new credentials without reconfiguration." simple_io_instruction(:change_ownership) @doc "Check for OS updates." simple_io_instruction(:check_updates) @doc "Create a diagnostic dump of information." simple_io_instruction(:dump_info) @doc "Move to a location offset by another location." def move_absolute(%FarmProc{} = farm_proc) do pc = get_pc_ptr(farm_proc) heap = get_heap_by_page_index(farm_proc, pc.page_address) data = AST.unslice(heap, pc.heap_address) data = case data.args.location do %AST{kind: :identifier} -> location = Resolver.resolve(farm_proc, pc, data.args.location.args.label) %{data | args: %{data.args | location: location}} _ -> data end data = case data.args.offset do %AST{kind: :identifier} -> offset = Resolver.resolve(farm_proc, pc, data.args.offset.args.label) %{data | args: %{data.args | offset: offset}} _ -> data end case farm_proc.io_result do # If we need to lookup a coordinate, do that. # We will come back to {:ok, ast} or {:error, reason} # next iteration. # Or if we didn't need to lookup a coordinate, just execute `move_absolute` # and come back to `:ok` or `{:error, reason}` nil -> latch = apply_sys_call_fun(farm_proc.sys_call_fun, data) farm_proc |> set_status(:waiting) |> set_io_latch(latch) # Result of coordinate lookup. # This starts the actual movement. {:ok, %AST{} = result} -> args = AST.new(:move_absolute, %{location: result, offset: data.args.offset}, []) latch = apply_sys_call_fun(farm_proc.sys_call_fun, args) farm_proc |> set_status(:waiting) |> set_io_latch(latch) # Result of _actual_ movement. :ok -> next_or_return(farm_proc) {:error, reason} -> crash(farm_proc, reason) other -> exception(farm_proc, "Bad return value handling move_absolute IO: #{inspect(other)}") end end def rpc_request(%FarmProc{} = farm_proc) do sequence(farm_proc) end @doc "Execute a sequeence." @spec sequence(FarmProc.t()) :: FarmProc.t() def sequence(%FarmProc{} = farm_proc) do pc_ptr = get_pc_ptr(farm_proc) body_addr = get_body_address(farm_proc, pc_ptr) if is_null_address?(body_addr), do: return(farm_proc), else: call(farm_proc, body_addr) end @doc "Conditionally execute a sequence." @spec _if(FarmProc.t()) :: FarmProc.t() def _if(%FarmProc{io_result: nil} = farm_proc) do pc = get_pc_ptr(farm_proc) heap = get_heap_by_page_index(farm_proc, pc.page_address) data = Farmbot.CeleryScript.AST.Unslicer.run(heap, pc.heap_address) latch = apply_sys_call_fun(farm_proc.sys_call_fun, data) farm_proc |> set_status(:waiting) |> set_io_latch(latch) end def _if(%FarmProc{io_result: result} = farm_proc) do pc = get_pc_ptr(farm_proc) case result do {:ok, true} -> farm_proc |> set_pc_ptr(get_cell_attr_as_pointer(farm_proc, pc, :___then)) |> clear_io_result() {:ok, false} -> farm_proc |> set_pc_ptr(get_cell_attr_as_pointer(farm_proc, pc, :___else)) |> clear_io_result() :ok -> exception(farm_proc, "Bad _if implementation.") {:error, reason} -> crash(farm_proc, reason) end end @doc "Do nothing. Triggers `status` to be set to `done`." @spec nothing(FarmProc.t()) :: FarmProc.t() def nothing(%FarmProc{} = farm_proc) do next_or_return(farm_proc) end @doc "Lookup and execute another sequence." @spec execute(FarmProc.t()) :: FarmProc.t() def execute(%FarmProc{io_result: nil} = farm_proc) do pc = get_pc_ptr(farm_proc) heap = get_heap_by_page_index(farm_proc, pc.page_address) sequence_id = FarmProc.get_cell_attr(farm_proc, pc, :sequence_id) next_ptr = get_next_address(farm_proc, pc) if FarmProc.has_page?(farm_proc, addr(sequence_id)) do farm_proc |> push_rs(next_ptr) |> set_pc_ptr(ptr(sequence_id, 1)) else # Step 0: Unslice current address. data = AST.unslice(heap, pc.heap_address) latch = apply_sys_call_fun(farm_proc.sys_call_fun, data) farm_proc |> set_status(:waiting) |> set_io_latch(latch) end end def execute(%FarmProc{io_result: result} = farm_proc) do pc = get_pc_ptr(farm_proc) sequence_id = FarmProc.get_cell_attr(farm_proc, pc, :sequence_id) next_ptr = get_next_address(farm_proc, pc) # Step 1: Get a copy of the sequence. case result do {:ok, %AST{} = sequence} -> # Step 2: Push PC -> RS # Step 3: Slice it new_heap = AST.slice(sequence) seq_addr = addr(sequence_id) seq_ptr = ptr(sequence_id, 1) push_rs(farm_proc, next_ptr) # Step 4: Add the new page. |> new_page(seq_addr, new_heap) # Step 5: Set PC to Ptr(1, 1) |> set_pc_ptr(seq_ptr) |> clear_io_result() {:error, reason} -> crash(farm_proc, reason) _ -> exception(farm_proc, "Bad execute implementation.") end end ## Private @spec call(FarmProc.t(), Pointer.t()) :: FarmProc.t() defp call(%FarmProc{} = farm_proc, %Pointer{} = address) do current_pc = get_pc_ptr(farm_proc) next_ptr = get_next_address(farm_proc, current_pc) farm_proc |> push_rs(next_ptr) |> set_pc_ptr(address) end @spec return(FarmProc.t()) :: FarmProc.t() defp return(%FarmProc{} = farm_proc) do {value, farm_proc} = pop_rs(farm_proc) farm_proc |> set_pc_ptr(value) |> set_status(:ok) end @spec next(FarmProc.t()) :: FarmProc.t() defp next(%FarmProc{} = farm_proc) do current_pc = get_pc_ptr(farm_proc) next_ptr = get_next_address(farm_proc, current_pc) farm_proc |> set_pc_ptr(next_ptr) |> set_status(:ok) end @spec next_or_return(FarmProc.t()) :: FarmProc.t() defp next_or_return(farm_proc) do pc_ptr = get_pc_ptr(farm_proc) addr = get_next_address(farm_proc, pc_ptr) farm_proc = clear_io_result(farm_proc) is_null_address? = is_null_address?(addr) return_stack_is_empty? = farm_proc.rs == [] cond do is_null_address? && return_stack_is_empty? -> set_status(farm_proc, :done) is_null_address? -> return(farm_proc) !is_null_address? -> next(farm_proc) end end @spec crash(FarmProc.t(), String.t()) :: FarmProc.t() defp crash(farm_proc, reason) do crash_address = get_pc_ptr(farm_proc) zero_page_ptr = get_zero_page(farm_proc) |> Pointer.null() # Push PC -> RS farm_proc |> push_rs(crash_address) # set PC to 0,0 |> set_pc_ptr(zero_page_ptr) # Set status to crashed, return the farmproc |> set_status(:crashed) |> set_crash_reason(reason) end end