207 lines
6.0 KiB
Elixir
207 lines
6.0 KiB
Elixir
defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.PinBinding do
|
|
@moduledoc """
|
|
Worker for monitoring hardware GPIO. (not related to the mcu firmware.)
|
|
|
|
Upon a button trigger, a `sequence`, or `special_action` will be executed by
|
|
the CeleryScript Runtime.
|
|
|
|
This module also defines a behaviour that allows for abstracting and testing
|
|
independent of GPIO hardware code.
|
|
"""
|
|
|
|
use GenServer
|
|
require Logger
|
|
require FarmbotCore.Logger
|
|
|
|
alias FarmbotCore.{
|
|
Asset.PinBinding,
|
|
Asset.Sequence,
|
|
Asset
|
|
}
|
|
|
|
alias FarmbotCeleryScript.AST
|
|
|
|
# @error_retry_time_ms Application.get_env(:farmbot_core, __MODULE__)[:error_retry_time_ms]
|
|
@error_retry_time_ms 5000
|
|
|
|
@gpio_handler Application.get_env(:farmbot_core, __MODULE__)[:gpio_handler]
|
|
@gpio_handler ||
|
|
Mix.raise("""
|
|
config :farmbot_core, #{__MODULE__}, gpio_handler: MyModule
|
|
""")
|
|
|
|
@error_retry_time_ms ||
|
|
Mix.raise("""
|
|
config :farmbot_core, #{__MODULE__}, error_retry_time_ms: 30_000
|
|
""")
|
|
|
|
@typedoc "Opaque function that should be called upon a trigger"
|
|
@type trigger_fun :: (pid -> any)
|
|
|
|
@typedoc "Integer representing a GPIO on the target platform."
|
|
@type pin_number :: integer
|
|
|
|
@doc """
|
|
Start a GPIO Handler. Returns the same values as a GenServer start.
|
|
|
|
Should call `#{__MODULE__}.trigger/1` when a pin has been triggered.
|
|
"""
|
|
@callback start_link(pin_number, trigger_fun) :: GenServer.on_start()
|
|
|
|
@impl true
|
|
def preload(%PinBinding{}), do: []
|
|
|
|
@impl true
|
|
def tracks_changes?(%PinBinding{}), do: false
|
|
|
|
@impl true
|
|
def start_link(%PinBinding{} = pin_binding, _args) do
|
|
GenServer.start_link(__MODULE__, %PinBinding{} = pin_binding)
|
|
end
|
|
|
|
# This function is opaque and should be considered private.
|
|
@doc false
|
|
def trigger(pid) do
|
|
GenServer.cast(pid, :trigger)
|
|
end
|
|
|
|
@impl true
|
|
def init(%PinBinding{} = pin_binding) do
|
|
{:ok, %{pin_binding: pin_binding}, 0}
|
|
end
|
|
|
|
@impl true
|
|
def handle_cast(:trigger, %{pin_binding: %{special_action: nil} = pin_binding} = state) do
|
|
case Asset.get_sequence(pin_binding.sequence_id) do
|
|
%Sequence{name: name} = seq ->
|
|
FarmbotCore.Logger.info(1, "#{pin_binding} triggered, executing #{name}")
|
|
|
|
AST.decode(seq)
|
|
|> execute(state)
|
|
|
|
nil ->
|
|
FarmbotCore.Logger.error(1, "Failed to find assosiated Sequence for: #{pin_binding}")
|
|
{:noreply, state}
|
|
end
|
|
end
|
|
|
|
def handle_cast(
|
|
:trigger,
|
|
%{pin_binding: %{special_action: "emergency_lock"} = pin_binding} = state
|
|
) do
|
|
FarmbotCore.Logger.info(1, "#{pin_binding} triggered, executing Emergency Lock")
|
|
|
|
AST.Factory.new()
|
|
|> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}")
|
|
|> AST.Factory.emergency_lock()
|
|
|> execute(state)
|
|
end
|
|
|
|
def handle_cast(
|
|
:trigger,
|
|
%{pin_binding: %{special_action: "emergency_unlock"} = pin_binding} = state
|
|
) do
|
|
FarmbotCore.Logger.info(1, "#{pin_binding} triggered, executing Emergency Unlock")
|
|
|
|
AST.Factory.new()
|
|
|> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}")
|
|
|> AST.Factory.emergency_unlock()
|
|
|> execute(state)
|
|
end
|
|
|
|
def handle_cast(:trigger, %{pin_binding: %{special_action: "power_off"} = pin_binding} = state) do
|
|
FarmbotCore.Logger.info(1, "#{pin_binding} triggered, executing Power Off")
|
|
|
|
AST.Factory.new()
|
|
|> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}")
|
|
|> AST.Factory.power_off()
|
|
|> execute(state)
|
|
end
|
|
|
|
def handle_cast(
|
|
:trigger,
|
|
%{pin_binding: %{special_action: "read_status"} = pin_binding} = state
|
|
) do
|
|
FarmbotCore.Logger.info(1, "#{pin_binding} triggered, executing Read Status")
|
|
|
|
AST.Factory.new()
|
|
|> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}")
|
|
|> AST.Factory.read_status()
|
|
|> execute(state)
|
|
end
|
|
|
|
def handle_cast(:trigger, %{pin_binding: %{special_action: "reboot"} = pin_binding} = state) do
|
|
FarmbotCore.Logger.info(1, "#{pin_binding} triggered, executing Reboot")
|
|
|
|
AST.Factory.new()
|
|
|> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}")
|
|
|> AST.Factory.reboot()
|
|
|> execute(state)
|
|
end
|
|
|
|
def handle_cast(:trigger, %{pin_binding: %{special_action: "sync"} = pin_binding} = state) do
|
|
FarmbotCore.Logger.info(1, "#{pin_binding} triggered, executing Sync")
|
|
|
|
AST.Factory.new()
|
|
|> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}")
|
|
|> AST.Factory.sync()
|
|
|> execute(state)
|
|
end
|
|
|
|
def handle_cast(:trigger, %{pin_binding: %{special_action: "take_photo"} = pin_binding} = state) do
|
|
FarmbotCore.Logger.info(1, "#{pin_binding} triggered, executing Take Photo")
|
|
|
|
AST.Factory.new()
|
|
|> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}")
|
|
|> AST.Factory.take_photo()
|
|
|> execute(state)
|
|
end
|
|
|
|
def handle_cast(:trigger, %{pin_binding: pin_binding} = state) do
|
|
FarmbotCore.Logger.error(1, "Unknown PinBinding: #{pin_binding}")
|
|
{:noreply, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_info(:timeout, %{pin_binding: pin_binding} = state) do
|
|
worker_pid = self()
|
|
|
|
case gpio_handler().start_link(pin_binding.pin_num, fn -> trigger(worker_pid) end) do
|
|
{:ok, pid} when is_pid(pid) ->
|
|
Process.link(pid)
|
|
{:noreply, state}
|
|
|
|
{:error, {:already_started, pid}} ->
|
|
Process.link(pid)
|
|
{:noreply, state}
|
|
|
|
{:error, reason} ->
|
|
Logger.error("Failed to start PinBinding GPIO Handler: #{inspect(reason)}")
|
|
{:noreply, state, @error_retry_time_ms}
|
|
|
|
:ignore ->
|
|
Logger.info("Failed to start PinBinding GPIO Handler. Not retrying.")
|
|
{:noreply, state}
|
|
end
|
|
end
|
|
|
|
def handle_info({:step_complete, _ref, _result}, state) do
|
|
{:noreply, state}
|
|
end
|
|
|
|
defp execute(%AST{} = ast, state) do
|
|
case FarmbotCeleryScript.execute(ast, make_ref()) do
|
|
:ok ->
|
|
:ok
|
|
|
|
{:error, reason} ->
|
|
FarmbotCore.Logger.error(1, "error executing #{state.pin_binding}: #{reason}")
|
|
end
|
|
|
|
{:noreply, state}
|
|
end
|
|
|
|
defp gpio_handler,
|
|
do: Application.get_env(:farmbot_core, __MODULE__)[:gpio_handler]
|
|
end
|