Add docs for farmware and http

This commit is contained in:
Connor Rigby 2017-11-02 09:52:23 -07:00
parent 7953d2100d
commit 46232d11d9
5 changed files with 202 additions and 2 deletions

View file

@ -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__])

View file

@ -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}

View file

@ -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."

View 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

View 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