Add docs for farmware and http
This commit is contained in:
parent
7953d2100d
commit
46232d11d9
|
@ -27,6 +27,11 @@ defmodule Farmbot.BotState do
|
|||
GenStage.call(__MODULE__, :force_state_push)
|
||||
end
|
||||
|
||||
def get_user_env do
|
||||
# GenStage.call(__MODULE__, :get_user_env)
|
||||
%{}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def start_link() do
|
||||
GenStage.start_link(__MODULE__, [], [name: __MODULE__])
|
||||
|
|
|
@ -1,5 +1,20 @@
|
|||
defmodule Farmbot.BotState.Transport.HTTP do
|
||||
@moduledoc "Transport for accepting CS and pushing state over HTTP."
|
||||
@moduledoc """
|
||||
RESTful API for accessing internal Farmbot state.
|
||||
|
||||
# Accessing the API
|
||||
A developer should be able to access the REST API at
|
||||
`http://<my_farmbot_id>:27347/api/v1/`. The calls will require an authentication token.
|
||||
See the [API docs](https://github.com/farmbot/Farmbot-Web-App#q-how-can-i-generate-an-api-token)
|
||||
for information about generating a token. Access to the local api should be
|
||||
the same as accessing the cloud API. You will need to have an HTTP header:
|
||||
`Authorization`:`Bearer <long encrypted token>`
|
||||
|
||||
Each of the routes will be described below.
|
||||
|
||||
* GET `/api/v1/bot/state` - returns the bot's current state.
|
||||
"""
|
||||
|
||||
use GenStage
|
||||
require Logger
|
||||
alias Farmbot.BotState.Transport.HTTP.{Router, SocketHandler}
|
||||
|
|
|
@ -1,5 +1,88 @@
|
|||
defmodule Farmbot.Farmware do
|
||||
@moduledoc "Farmware is Farmbot's plugin system."
|
||||
@moduledoc """
|
||||
Farmware is Farmbot's plugin system. Developing a farmware is simple.
|
||||
You will need 3 things:
|
||||
* A `manifest.json` hosted on the internet somewhere.
|
||||
* A zip package of your farmware.
|
||||
|
||||
# Farmware Manifest
|
||||
The `manifest.json` file should contain a number of required fields.
|
||||
* `package` - the name of your package. Should be CamelCase by convention.
|
||||
* `version` - The version of your package. [Semver](http://semver.org/) is required here.
|
||||
* `min_os_version_major` - A version requirement for Farmbot OS.
|
||||
* `url` - A url that points to this file.
|
||||
* `zip` - A url to the zip package to be downloaded and installed.
|
||||
* `executable` - the binary file that will be executed.
|
||||
* `args` - An array of strings that will be passed to the `executable`.
|
||||
* `config` - A set of default values to be passed to the Farmware.
|
||||
see [Configuration](#Configuration) for more details
|
||||
|
||||
There are also a number of metadata fields that are not required, but are
|
||||
highly suggested.
|
||||
* `author` - The author of this package.
|
||||
* `description` - A brief description of the package.
|
||||
* `language` - The language that this plugin was developed in.
|
||||
|
||||
# The zip package
|
||||
The zip package is simply a zip file that contains all your assets. This
|
||||
will usually contain a executable of some sort, and anything required
|
||||
to enable the execution of your package.
|
||||
|
||||
# Repositories
|
||||
If you have more than one Farmware, and you would like to install all of them
|
||||
at one time, it might be worth it to put them in a `repository`. You will need
|
||||
to host a special `manifest.json` file that simply contains a list of objects.
|
||||
the objects should contain the following keys:
|
||||
* `manifest` - This should be a url that points to the package manifest.
|
||||
* `name` - the name of the package to install.
|
||||
|
||||
# Developing a Farmware
|
||||
Farmwares should be simple script-like programs. They block other access to the
|
||||
bot for their lifetime, so they should also be short lived.
|
||||
|
||||
## Communication
|
||||
Since Farmbot can not have two way communication with a Farmware, If you Farmware
|
||||
needs to communicate with Farmbot, you will need to use one of:
|
||||
* HTTP - [Docs](Farmbot.BotState.Transport.HTTP.html)
|
||||
* Easy to work with.
|
||||
* Allows call/response type functionality.
|
||||
* Requires polling for state updates.
|
||||
* No access to logs.
|
||||
* Raw Websockets - [Docs](Farmbot.BotState.Transport.HTTP.SocketHandler.html)
|
||||
* More difficult to work with.
|
||||
* Not exactly call/response.
|
||||
* No pollinig for state updates.
|
||||
* Logs come in real time.
|
||||
|
||||
# Configuration
|
||||
Since Farmbot and the Farmware can not talk directly, all configuration data
|
||||
is sent via Unix Environment Variables.
|
||||
|
||||
*NOTE: All values denoted by a `$` mean they are a environment variable, the
|
||||
actual variable name will not contain the `$`.*
|
||||
|
||||
There are two of default configuration's that can not be changed.
|
||||
* `$API_TOKEN` - An encoded binary that can be used to communicate with
|
||||
Farmbot and it's configured cloud server.
|
||||
* `$IMAGES_DIR` - A local directory that will be scanned. Photos left in this
|
||||
directory will be uploaded and visable from the web app.
|
||||
|
||||
Additional configuration can be supplied via the manifest. This is handy for
|
||||
configurating default values for a Farmware. the `config` field on the manifest
|
||||
should be an array of objects with these keys:
|
||||
* `name` - The name of the config.
|
||||
* `label` - The label that will show up on the web app.
|
||||
* `value` - The default value.
|
||||
|
||||
When your Farmware executes you will have these keys available, but they will
|
||||
be namespaced to your Farmware `package` name in snake case.
|
||||
For Example, if you have a farmware called "HelloFarmware", and it has a config:
|
||||
`{"name": "first_config", "label": "a config field", "value": 100}`, when your
|
||||
Farmware executes, it will have a key by the name of `$hello_farmware_first_config`
|
||||
that will have the value `100`.
|
||||
|
||||
Config values can however be overwrote by the Farmbot App.
|
||||
"""
|
||||
|
||||
defmodule Meta do
|
||||
@moduledoc "Metadata about a Farmware."
|
||||
|
|
82
lib/farmbot/farmware/runtime.ex
Normal file
82
lib/farmbot/farmware/runtime.ex
Normal file
|
@ -0,0 +1,82 @@
|
|||
defmodule Farmbot.Farmware.Runtime do
|
||||
@moduledoc "Handles execution of a Farmware."
|
||||
|
||||
alias Farmbot.Farmware
|
||||
alias Farmware.{RuntimeError, Installer}
|
||||
require Logger
|
||||
|
||||
defmodule State do
|
||||
@moduledoc false
|
||||
defstruct [:farmware, :env, :port, :exit_status, :working_dir, :return_dir]
|
||||
end
|
||||
|
||||
@doc "Execute a Farmware struct."
|
||||
def execute(%Farmware{} = farmware) do
|
||||
Logger.info "Beginning execution of #{inspect farmware}"
|
||||
fw_path = Installer.install_path(farmware) |> Path.absname("#{:code.priv_dir(:farmbot)}/..")
|
||||
with {:ok, cwd} <- File.cwd(),
|
||||
:ok <- File.cd(fw_path),
|
||||
env <- build_env(farmware)
|
||||
do
|
||||
exec = farmware.executable
|
||||
opts = [:stream,
|
||||
:binary,
|
||||
:exit_status,
|
||||
:hide,
|
||||
:use_stdio,
|
||||
:stderr_to_stdout,
|
||||
args: farmware.args,
|
||||
env: env ]
|
||||
port = Port.open({:spawn_executable, exec}, opts)
|
||||
handle_port(struct(State, [port: port, env: env, farmware: farmware, working_dir: fw_path, return_dir: cwd]))
|
||||
else
|
||||
{:error, err} -> raise RuntimeError, [state: nil, message: err]
|
||||
end
|
||||
|> do_cleanup()
|
||||
end
|
||||
|
||||
defp do_cleanup(%State{return_dir: return_dir} = state) do
|
||||
File.cd(return_dir)
|
||||
state
|
||||
end
|
||||
|
||||
defp handle_port(%State{port: port, farmware: farmware} = state) do
|
||||
receive do
|
||||
{^port, {:exit_status, 0}} ->
|
||||
Logger.info "#{inspect farmware} completed without errors."
|
||||
%{state | exit_status: 0}
|
||||
{^port, {:exit_status, status}} ->
|
||||
Logger.warn "#{inspect farmware} completed with exit status: #{status}"
|
||||
%{state | exit_status: status}
|
||||
{^port, {:data, data}} ->
|
||||
IO.puts "[#{inspect farmware}] sent data: \r\n===========\r\n\r\n#{data} \r\n==========="
|
||||
handle_port(state)
|
||||
end
|
||||
end
|
||||
|
||||
defp build_env(%Farmware{config: config, name: fw_name} = _farmware) do
|
||||
token = Farmbot.System.ConfigStorage.get_config_value(:string, "authorization", "token")
|
||||
images_dir = "/tmp/images"
|
||||
|
||||
config
|
||||
|> Enum.filter(&match?(%{"label" => _, "name" => _, "value" => _}, &1))
|
||||
|> Map.new(&format_config(fw_name, &1))
|
||||
|> Map.put("API_TOKEN", token)
|
||||
|> Map.put("IMAGES_DIR", images_dir)
|
||||
|> Map.merge(Farmbot.BotState.get_user_env())
|
||||
|> Enum.map(fn({key, val}) -> {to_erl_safe(key), to_erl_safe(val)} end)
|
||||
end
|
||||
|
||||
defp format_config(fw_name, %{"label" => _, "name" => name, "value" => val}) do
|
||||
sep = case String.contains?(fw_name, "-") do
|
||||
true -> "-"
|
||||
false -> " "
|
||||
end
|
||||
ns = String.split(fw_name, sep) |> Enum.join() |> Macro.underscore
|
||||
{"#{ns}_#{name}", val}
|
||||
end
|
||||
|
||||
defp to_erl_safe(binary) when is_binary(binary), do: to_charlist(binary)
|
||||
defp to_erl_safe(map) when is_map(map), do: map |> Poison.encode! |> to_erl_safe()
|
||||
defp to_erl_safe(number) when is_number(number), do: number
|
||||
end
|
15
lib/farmbot/farmware/runtime_error.ex
Normal file
15
lib/farmbot/farmware/runtime_error.ex
Normal file
|
@ -0,0 +1,15 @@
|
|||
defmodule Farmbot.Farmware.RuntimeError do
|
||||
@moduledoc "Error executing a Farmware."
|
||||
|
||||
defexception [:message, :state]
|
||||
|
||||
@doc false
|
||||
def exception(opts) do
|
||||
struct(__MODULE__, [message: Keyword.fetch!(opts, :message), state: Keyword.fetch!(opts, :state)])
|
||||
end
|
||||
|
||||
@doc false
|
||||
def message(%__MODULE__{message: m}), do: m |> to_string()
|
||||
|
||||
|
||||
end
|
Loading…
Reference in a new issue