commit
e43026a8af
|
@ -1,76 +1,156 @@
|
|||
version: 2.0
|
||||
defaults: &defaults
|
||||
working_directory: ~/farmbot_os
|
||||
docker:
|
||||
- image: nervesproject/nerves:0.13.5
|
||||
environment:
|
||||
ENV: CI
|
||||
MIX_ENV: test
|
||||
ELIXIR_VERSION: 1.5.2
|
||||
|
||||
install_elixir: &install_elixir
|
||||
run:
|
||||
name: Install Elixir
|
||||
command: |
|
||||
wget https://github.com/elixir-lang/elixir/releases/download/v$ELIXIR_VERSION/Precompiled.zip
|
||||
wget https://github.com/elixir-lang/elixir/releases/download/v1.5.2/Precompiled.zip
|
||||
unzip -d /usr/local/elixir Precompiled.zip
|
||||
echo 'export PATH=/usr/local/elixir/bin:$PATH' >> $BASH_ENV
|
||||
install_hex_rebar: &install_hex_rebar
|
||||
|
||||
install_hex_archives: &install_hex_archives
|
||||
run:
|
||||
name: Install hex and rebar
|
||||
name: Install archives
|
||||
command: |
|
||||
mix local.hex --force
|
||||
mix local.rebar --force
|
||||
install_nerves_bootstrap: &install_nerves_bootstrap
|
||||
mix archive.install hex nerves_bootstrap --force
|
||||
|
||||
fetch_and_compile_deps: &fetch_and_compile_deps
|
||||
run:
|
||||
name: Install nerves_bootstrap
|
||||
command: mix archive.install hex nerves_bootstrap --force
|
||||
name: Fetch and compile Elixir dependencies
|
||||
command: |
|
||||
mix deps.get
|
||||
mix deps.compile
|
||||
mix compile
|
||||
|
||||
jobs:
|
||||
test:
|
||||
<<: *defaults
|
||||
environment:
|
||||
MIX_ENV: test
|
||||
MIX_TARGET: host
|
||||
steps:
|
||||
- checkout
|
||||
- <<: *install_elixir
|
||||
- <<: *install_hex_rebar
|
||||
- <<: *install_nerves_bootstrap
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v3-dependency-cache-{{ checksum "mix.lock.host" }}
|
||||
- v3-dependency-cache
|
||||
- <<: *install_hex_archives
|
||||
- <<: *fetch_and_compile_deps
|
||||
- save_cache:
|
||||
key: v3-dependency-cache-{{ checksum "mix.lock.host" }}
|
||||
paths:
|
||||
- _build
|
||||
- ~/.mix
|
||||
- ~/.nerves
|
||||
- run:
|
||||
name: Install test dependencies
|
||||
command: mix deps.get
|
||||
- run:
|
||||
name: Compile
|
||||
command: mix compile
|
||||
- run:
|
||||
name: Lint
|
||||
command: mix credo --strict --ignore Credo.Check.Readability.MaxLineLength
|
||||
- run:
|
||||
name: Test
|
||||
command: mix coveralls.circle --seed 0 --exclude farmbot_firmware
|
||||
command: mix coveralls.circle --exclude farmbot_firmware
|
||||
|
||||
firmware:
|
||||
firmware_dev:
|
||||
<<: *defaults
|
||||
environment:
|
||||
MIX_ENV: dev
|
||||
MIX_TARGET: rpi3
|
||||
ELIXIR_VERSION: 1.5.2
|
||||
MIX_ENV: dev
|
||||
ENV: CI
|
||||
steps:
|
||||
- checkout
|
||||
- <<: *install_elixir
|
||||
- <<: *install_hex_rebar
|
||||
- <<: *install_nerves_bootstrap
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v3-dependency-cache-{{ checksum "mix.lock.rpi3" }}
|
||||
- v3-dependency-cache
|
||||
- <<: *install_hex_archives
|
||||
- <<: *fetch_and_compile_deps
|
||||
- run: mix firmware
|
||||
- save_cache:
|
||||
key: v3-dependency-cache-{{ checksum "mix.lock.rpi3" }}
|
||||
paths:
|
||||
- _build
|
||||
- ~/.mix
|
||||
- ~/.nerves
|
||||
- run: mix firmware.slack --channels C58DCU4A3
|
||||
- run: mkdir -p artifacts
|
||||
- run:
|
||||
name: Set COMMIT_MESSAGE var
|
||||
command: echo "export COMMIT_MESSAGE=\"$(git log --format=oneline -n 1 $CIRCLE_SHA1)\"" >> ~/.bashrc
|
||||
name: Decode fwup priv key
|
||||
command: echo $FWUP_KEY_BASE64 | base64 --decode --ignore-garbage > $NERVES_FW_PRIV_KEY
|
||||
- run:
|
||||
name: Install dev dependencies
|
||||
command: mix deps.get
|
||||
name: Sign firmware
|
||||
command: fwup -S -s $NERVES_FW_PRIV_KEY -i _build/${MIX_TARGET}/${MIX_ENV}/nerves/images/farmbot.fw -o artifacts/farmbot-${MIX_TARGET}-$(cat VERSION)-beta.fw
|
||||
- save_cache:
|
||||
key: v3-firmware-{{ .Revision }}-{{ .Environment.CIRCLE_TAG }}
|
||||
paths:
|
||||
- ./artifacts
|
||||
|
||||
firmware_prod:
|
||||
<<: *defaults
|
||||
environment:
|
||||
MIX_TARGET: rpi3
|
||||
MIX_ENV: prod
|
||||
ENV: CI
|
||||
steps:
|
||||
- checkout
|
||||
- <<: *install_elixir
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v3-dependency-cache-{{ checksum "mix.lock.rpi3" }}
|
||||
- v3-dependency-cache
|
||||
- <<: *install_hex_archives
|
||||
- run: mix deps.get
|
||||
- run: mix deps.compile
|
||||
- run: mix compile
|
||||
- run: mix firmware
|
||||
- save_cache:
|
||||
key: v3-dependency-cache-{{ checksum "mix.lock.rpi3" }}
|
||||
paths:
|
||||
- _build
|
||||
- ~/.mix
|
||||
- ~/.nerves
|
||||
- run: mkdir -p artifacts
|
||||
- run:
|
||||
name: Build dev firmware
|
||||
command: mix firmware
|
||||
name: Decode fwup priv key
|
||||
command: echo $FWUP_KEY_BASE64 | base64 --decode --ignore-garbage > $NERVES_FW_PRIV_KEY
|
||||
- run:
|
||||
name: Upload dev firmware to slack
|
||||
command: mix firmware.slack --channels C58DCU4A3 $COMMIT_MESSAGE
|
||||
name: Sign firmware
|
||||
command: fwup -S -s $NERVES_FW_PRIV_KEY -i _build/${MIX_TARGET}/${MIX_ENV}/nerves/images/farmbot.fw -o artifacts/farmbot-${MIX_TARGET}-$(cat VERSION)-beta.fw
|
||||
- save_cache:
|
||||
key: v3-firmware-{{ .Revision }}-{{ .Environment.CIRCLE_TAG }}
|
||||
paths:
|
||||
- ./artifacts
|
||||
|
||||
deploy_firmware:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Run setup script
|
||||
command: bash .circleci/setup-heroku.sh
|
||||
- add_ssh_keys:
|
||||
fingerprints:
|
||||
- "97:92:32:5d:d7:96:e1:fa:f3:6b:f3:bd:d6:aa:84:c6"
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: |
|
||||
wget https://github.com/tcnksm/ghr/releases/download/v0.5.4/ghr_v0.5.4_linux_amd64.zip
|
||||
unzip ghr_v0.5.4_linux_amd64.zip
|
||||
wget https://github.com/stedolan/jq/releases/download/jq-1.5/jq-linux64
|
||||
chmod +x ./jq-linux64
|
||||
- run:
|
||||
command: grep -Pazo "(?s)(?<=# $(cat VERSION))[^#]+" CHANGELOG.md > RELEASE_NOTES
|
||||
- restore_cache:
|
||||
key: v3-firmware-{{ .Revision }}-{{ .Environment.CIRCLE_TAG }}
|
||||
- run:
|
||||
command: ./ghr -t $GITHUB_TOKEN -u farmbot -r farmbot_os -recreate -prerelease -b "$(cat RELEASE_NOTES)" -c $(git rev-parse --verify HEAD) "v$(cat VERSION)-beta" $PWD/artifacts
|
||||
- run:
|
||||
name: Update heroku env
|
||||
command: |
|
||||
export OTA_URL=$(wget https://api.github.com/repos/farmbot/farmbot_os/releases -qO- | ./jq-linux64 '.[0].url')
|
||||
heroku config:set BETA_OTA_URL=$OTA_URL --app=farmbot-production
|
||||
heroku config:set BETA_OTA_URL=$OTA_URL --app=farmbot-staging
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
|
@ -78,5 +158,30 @@ workflows:
|
|||
jobs:
|
||||
- test:
|
||||
context: org-global
|
||||
- firmware:
|
||||
filters:
|
||||
branches:
|
||||
ignore: beta
|
||||
- firmware_dev:
|
||||
context: org-global
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
branches:
|
||||
ignore: beta
|
||||
|
||||
deploy_beta:
|
||||
jobs:
|
||||
- firmware_prod:
|
||||
context: org-global
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- beta
|
||||
- deploy_firmware:
|
||||
context: org-global
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- beta
|
||||
requires:
|
||||
- firmware_prod
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
#!/bin/bash
|
||||
wget https://cli-assets.heroku.com/branches/stable/heroku-linux-amd64.tar.gz
|
||||
mkdir -p /usr/local/lib /usr/local/bin
|
||||
tar -xvzf heroku-linux-amd64.tar.gz -C /usr/local/lib
|
||||
ln -s /usr/local/lib/heroku/bin/heroku /usr/local/bin/heroku
|
||||
|
||||
cat > ~/.netrc << EOF
|
||||
machine api.heroku.com
|
||||
login $HEROKU_LOGIN
|
||||
password $HEROKU_API_KEY
|
||||
EOF
|
||||
|
||||
cat >> ~/.ssh/config << EOF
|
||||
VerifyHostKeyDNS yes
|
||||
StrictHostKeyChecking no
|
||||
EOF
|
|
@ -1,2 +1,2 @@
|
|||
erlang 20.0
|
||||
erlang 20.1
|
||||
elixir 1.5.2
|
||||
|
|
18
CHANGELOG.md
18
CHANGELOG.md
|
@ -113,3 +113,21 @@
|
|||
|
||||
# 5.0.9
|
||||
* Add missing redis-py package for Farmware.
|
||||
|
||||
# 6.0.1
|
||||
* Add feature auto sync.
|
||||
* Add feature RPI GPIO.
|
||||
* Refactor Configurator to not need Javascript/Webpack
|
||||
* Add timer before network not found factory resets bot.
|
||||
* Remove steps/mm conversion.
|
||||
* Bundle new arduino-firmware.
|
||||
* Replace MQTT with AMQP.
|
||||
* Get rid of Log batching.
|
||||
* Add verbosity level to _every_ log message.
|
||||
* Show position for log messages.
|
||||
* Add many helpful log messages.
|
||||
* Add feature to disable many log message.
|
||||
* Add feature to log all arduino-firmware I/O.
|
||||
* Migrated CI to CircleCI from TravisCI.
|
||||
* Refactored FarmEvent Calendar generator.
|
||||
* Fix a ton of little bugs.
|
||||
|
|
|
@ -64,12 +64,6 @@ config :farmbot, :behaviour,
|
|||
update_handler: Farmbot.Target.UpdateHandler,
|
||||
gpio_handler: Farmbot.Target.GPIO.AleHandler
|
||||
|
||||
|
||||
config :nerves_firmware_ssh,
|
||||
authorized_keys: [
|
||||
File.read!(Path.join(System.user_home!, ".ssh/id_rsa.pub"))
|
||||
]
|
||||
|
||||
config :nerves_init_gadget,
|
||||
address_method: :static
|
||||
|
||||
|
|
|
@ -24,7 +24,9 @@ defmodule Farmbot.Bootstrap.AuthTask do
|
|||
end
|
||||
|
||||
def terminate(reason, _state) do
|
||||
Logger.error 1, "Token Refresh failed: #{inspect reason}"
|
||||
unless reason == {:shutdown, :normal} do
|
||||
Logger.error 1, "Token Refresh failed: #{inspect reason}"
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info(:refresh, _old_timer) do
|
||||
|
|
|
@ -49,7 +49,8 @@ defmodule Farmbot.BotState.Transport.AMQP do
|
|||
opts <- [conn: conn, chan: chan, queue_name: q_name, bot: device],
|
||||
state <- struct(State, opts)
|
||||
do
|
||||
Process.monitor(conn.pid)
|
||||
true = Process.link(conn.pid)
|
||||
true = Process.link(chan.pid)
|
||||
{:consumer, state, subscribe_to: [Farmbot.BotState, Farmbot.Logger]}
|
||||
else
|
||||
{:error, {:auth_failure, msg}} = fail ->
|
||||
|
@ -146,10 +147,6 @@ defmodule Farmbot.BotState.Transport.AMQP do
|
|||
{:noreply, [], state}
|
||||
end
|
||||
|
||||
def handle_info({:DOWN, _, :process, _pid, reason}, state) do
|
||||
{:stop, reason, state}
|
||||
end
|
||||
|
||||
# Confirmation sent by the broker after registering this process as a consumer
|
||||
def handle_info({:basic_consume_ok, _}, state) do
|
||||
if get_config_value(:bool, "settings", "log_amqp_connected") do
|
||||
|
@ -191,14 +188,27 @@ defmodule Farmbot.BotState.Transport.AMQP do
|
|||
end
|
||||
end
|
||||
|
||||
defp handle_celery_script(payload, _state) do
|
||||
def handle_info({:DOWN, _, :process, pid, reason}, state) do
|
||||
unless reason == :normal do
|
||||
Logger.warn 3, "CeleryScript: #{inspect pid} died: #{inspect reason}"
|
||||
end
|
||||
{:noreply, [], state}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_celery_script(payload, _state) do
|
||||
case AST.decode(payload) do
|
||||
{:ok, ast} -> spawn CeleryScript, :execute, [ast]
|
||||
{:ok, ast} ->
|
||||
pid = spawn(CeleryScript, :execute, [ast])
|
||||
# Logger.busy 3, "CeleryScript starting: #{inspect pid}"
|
||||
Process.monitor(pid)
|
||||
:ok
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_sync_cmd(kind, id, payload, state) do
|
||||
@doc false
|
||||
def handle_sync_cmd(kind, id, payload, state) do
|
||||
mod = Module.concat(["Farmbot", "Repo", kind])
|
||||
if Code.ensure_loaded?(mod) do
|
||||
%{
|
||||
|
@ -206,13 +216,13 @@ defmodule Farmbot.BotState.Transport.AMQP do
|
|||
"args" => %{"label" => uuid}
|
||||
} = Poison.decode!(payload, as: %{"body" => struct(mod)})
|
||||
|
||||
Farmbot.Repo.register_sync_cmd(String.to_integer(id), kind, body)
|
||||
:ok = Farmbot.Repo.register_sync_cmd(String.to_integer(id), kind, body)
|
||||
|
||||
if get_config_value(:bool, "settings", "auto_sync") do
|
||||
Farmbot.Repo.flip()
|
||||
end
|
||||
|
||||
AST.Node.RpcOk.execute(%{label: uuid}, [], struct(Macro.Env))
|
||||
{:ok, %Macro.Env{}} = AST.Node.RpcOk.execute(%{label: uuid}, [], struct(Macro.Env))
|
||||
else
|
||||
msg = "Unknown syncable: #{mod}: #{inspect Poison.decode!(payload)}"
|
||||
Logger.warn 2, msg
|
||||
|
|
|
@ -5,9 +5,10 @@ defmodule Farmbot.CeleryScript.AST.Node.CheckUpdates do
|
|||
|
||||
def execute(%{package: :farmbot_os}, _, env) do
|
||||
env = mutate_env(env)
|
||||
case Farmbot.System.Updates.check_updates() do
|
||||
case Farmbot.System.Updates.check_updates(true) do
|
||||
:ok -> {:ok, env}
|
||||
{:error, reason} -> {:error, reason, env}
|
||||
:no_update -> {:ok, env}
|
||||
_ -> {:error, "Failed to check updates", env}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -59,6 +59,7 @@ defmodule Farmbot.CeleryScript.AST.Node.ConfigUpdate do
|
|||
defp lookup_os_config("arduino_debug_messages", val), do: {:ok, {:bool, "settings", format_bool_for_os(val)}}
|
||||
defp lookup_os_config("firmware_input_log", val), do: {:ok, {:bool, "settings", format_bool_for_os(val)}}
|
||||
defp lookup_os_config("firmware_output_log", val), do: {:ok, {:bool, "settings", format_bool_for_os(val)}}
|
||||
defp lookup_os_config("beta_opt_in", val), do: {:ok, {:bool, "settings", format_bool_for_os(val)}}
|
||||
|
||||
defp lookup_os_config("network_not_found_timer", val) when val > 0, do: {:ok, {:float, "settings", to_float(val)}}
|
||||
defp lookup_os_config("network_not_found_timer", _val), do: {:error, "network_not_found_timer must be greater than zero"}
|
||||
|
|
|
@ -7,14 +7,10 @@ defmodule Farmbot.CeleryScript.AST.Node.WritePin do
|
|||
|
||||
def execute(%{pin_mode: mode, pin_value: value, pin_number: num}, [], env) do
|
||||
env = mutate_env(env)
|
||||
case Farmbot.Firmware.set_pin_mode(num, :output) do
|
||||
case Farmbot.Firmware.write_pin(num, mode, value) do
|
||||
:ok ->
|
||||
case Farmbot.Firmware.write_pin(num, mode, value) do
|
||||
:ok ->
|
||||
log_success(num, mode, value)
|
||||
{:ok, env}
|
||||
{:error, reason} -> {:error, reason, env}
|
||||
end
|
||||
log_success(num, mode, value)
|
||||
{:ok, env}
|
||||
{:error, reason} -> {:error, reason, env}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -31,7 +31,7 @@ defmodule Farmbot.CeleryScript.AST.Node.Zero do
|
|||
@default_num_tries 20
|
||||
defp do_wait_for_pos(axis, env, tries \\ @default_num_tries)
|
||||
|
||||
defp do_wait_for_pos(axis, env, 0) do
|
||||
defp do_wait_for_pos(_axis, env, 0) do
|
||||
# {:error, "Failed to set #{axis} location to 0", env}
|
||||
{:ok, env}
|
||||
end
|
||||
|
|
|
@ -68,7 +68,7 @@ defmodule Farmbot.FarmEvent.Manager do
|
|||
{:noreply, %{state | timer: nil, checkup: checkup}}
|
||||
end
|
||||
|
||||
def handle_info({:DOWN, _, :process, _, {:success, new_state}}, old_state) do
|
||||
def handle_info({:DOWN, _, :process, _, {:success, new_state}}, _old_state) do
|
||||
timer = Process.send_after(self(), :checkup, @checkup_time)
|
||||
{:noreply, %{new_state | timer: timer, checkup: nil}}
|
||||
end
|
||||
|
@ -79,7 +79,7 @@ defmodule Farmbot.FarmEvent.Manager do
|
|||
{:noreply, %{state | timer: timer, checkup: nil}}
|
||||
end
|
||||
|
||||
def async_checkup(manager, state) do
|
||||
def async_checkup(_manager, state) do
|
||||
now = get_now()
|
||||
alias Farmbot.Repo.FarmEvent
|
||||
# maybe_farm_event_log "Rebuilding calendar."
|
||||
|
|
|
@ -4,35 +4,40 @@ defmodule Farmbot.Firmware do
|
|||
use GenStage
|
||||
use Farmbot.Logger
|
||||
alias Farmbot.Firmware.Vec3
|
||||
import Farmbot.System.ConfigStorage, only: [update_config_value: 4, get_config_value: 3]
|
||||
|
||||
# If any command takes longer than this, exit.
|
||||
@call_timeout 500_000
|
||||
|
||||
@doc "Move the bot to a position."
|
||||
def move_absolute(%Vec3{} = vec3, x_speed, y_speed, z_speed) do
|
||||
GenStage.call(__MODULE__, {:move_absolute, [vec3, x_speed, y_speed, z_speed]}, :infinity)
|
||||
def move_absolute(%Vec3{} = vec3, x_spd, y_spd, z_spd) do
|
||||
call = {:move_absolute, [vec3, x_spd, y_spd, z_spd]}
|
||||
GenStage.call(__MODULE__, call, @call_timeout)
|
||||
end
|
||||
|
||||
@doc "Calibrate an axis."
|
||||
def calibrate(axis) do
|
||||
GenStage.call(__MODULE__, {:calibrate, [axis]}, :infinity)
|
||||
GenStage.call(__MODULE__, {:calibrate, [axis]}, @call_timeout)
|
||||
end
|
||||
|
||||
@doc "Find home on an axis."
|
||||
def find_home(axis) do
|
||||
GenStage.call(__MODULE__, {:find_home, [axis]}, :infinity)
|
||||
GenStage.call(__MODULE__, {:find_home, [axis]}, @call_timeout)
|
||||
end
|
||||
|
||||
@doc "Home every axis."
|
||||
def home_all() do
|
||||
GenStage.call(__MODULE__, {:home_all, []}, :infinity)
|
||||
GenStage.call(__MODULE__, {:home_all, []}, @call_timeout)
|
||||
end
|
||||
|
||||
@doc "Home an axis."
|
||||
def home(axis) do
|
||||
GenStage.call(__MODULE__, {:home, [axis]}, :infinity)
|
||||
GenStage.call(__MODULE__, {:home, [axis]}, @call_timeout)
|
||||
end
|
||||
|
||||
@doc "Manually set an axis's current position to zero."
|
||||
def zero(axis) do
|
||||
GenStage.call(__MODULE__, {:zero, [axis]}, :infinity)
|
||||
GenStage.call(__MODULE__, {:zero, [axis]}, @call_timeout)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
@ -40,12 +45,12 @@ defmodule Farmbot.Firmware do
|
|||
For a list of paramaters see `Farmbot.Firmware.Gcode.Param`
|
||||
"""
|
||||
def update_param(param, val) do
|
||||
GenStage.call(__MODULE__, {:update_param, [param, val]}, :infinity)
|
||||
GenStage.call(__MODULE__, {:update_param, [param, val]}, @call_timeout)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def read_all_params do
|
||||
GenStage.call(__MODULE__, {:read_all_params, []}, :infinity)
|
||||
GenStage.call(__MODULE__, {:read_all_params, []}, @call_timeout)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
@ -53,42 +58,42 @@ defmodule Farmbot.Firmware do
|
|||
For a list of paramaters see `Farmbot.Firmware.Gcode.Param`
|
||||
"""
|
||||
def read_param(param) do
|
||||
GenStage.call(__MODULE__, {:read_param, [param]}, :infinity)
|
||||
GenStage.call(__MODULE__, {:read_param, [param]}, @call_timeout)
|
||||
end
|
||||
|
||||
@doc "Emergency lock Farmbot."
|
||||
def emergency_lock() do
|
||||
GenStage.call(__MODULE__, {:emergency_lock, []}, :infinity)
|
||||
GenStage.call(__MODULE__, {:emergency_lock, []}, @call_timeout)
|
||||
end
|
||||
|
||||
@doc "Unlock Farmbot from Emergency state."
|
||||
def emergency_unlock() do
|
||||
GenStage.call(__MODULE__, {:emergency_unlock, []}, :infinity)
|
||||
GenStage.call(__MODULE__, {:emergency_unlock, []}, @call_timeout)
|
||||
end
|
||||
|
||||
@doc "Set a pin mode (:input | :output)"
|
||||
def set_pin_mode(pin, mode) do
|
||||
GenStage.call(__MODULE__, {:set_pin_mode, [pin, mode]}, :infinity)
|
||||
GenStage.call(__MODULE__, {:set_pin_mode, [pin, mode]}, @call_timeout)
|
||||
end
|
||||
|
||||
@doc "Read a pin."
|
||||
def read_pin(pin, mode) do
|
||||
GenStage.call(__MODULE__, {:read_pin, [pin, mode]}, :infinity)
|
||||
GenStage.call(__MODULE__, {:read_pin, [pin, mode]}, @call_timeout)
|
||||
end
|
||||
|
||||
@doc "Write a pin."
|
||||
def write_pin(pin, mode, value) do
|
||||
GenStage.call(__MODULE__, {:write_pin, [pin, mode, value]}, :infinity)
|
||||
GenStage.call(__MODULE__, {:write_pin, [pin, mode, value]}, @call_timeout)
|
||||
end
|
||||
|
||||
@doc "Request version."
|
||||
def request_software_version do
|
||||
GenStage.call(__MODULE__, {:request_software_version, []}, :infinity)
|
||||
GenStage.call(__MODULE__, {:request_software_version, []}, @call_timeout)
|
||||
end
|
||||
|
||||
@doc "Set angle of a servo pin."
|
||||
def set_servo_angle(pin, value) do
|
||||
GenStage.call(__MODULE__, {:set_servo_angle, [pin, value]}, :infinity)
|
||||
GenStage.call(__MODULE__, {:set_servo_angle, [pin, value]}, @call_timeout)
|
||||
end
|
||||
|
||||
@doc "Start the firmware services."
|
||||
|
@ -103,8 +108,7 @@ defmodule Farmbot.Firmware do
|
|||
defstruct [
|
||||
fun: nil,
|
||||
args: nil,
|
||||
from: nil,
|
||||
|
||||
from: nil
|
||||
]
|
||||
end
|
||||
|
||||
|
@ -114,6 +118,7 @@ defmodule Farmbot.Firmware do
|
|||
idle: false,
|
||||
timer: nil,
|
||||
pins: %{},
|
||||
params: %{},
|
||||
initialized: false,
|
||||
initializing: false,
|
||||
current: nil,
|
||||
|
@ -138,6 +143,15 @@ defmodule Farmbot.Firmware do
|
|||
|
||||
end
|
||||
|
||||
def terminate(reason, state) do
|
||||
unless :queue.is_empty(state.queue) do
|
||||
list = :queue.to_list(state.queue)
|
||||
for cmd <- list do
|
||||
:ok = do_reply(%{state | current: cmd}, {:error, reason})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info({:EXIT, _pid, :normal}, state) do
|
||||
{:stop, :normal, state}
|
||||
end
|
||||
|
@ -152,59 +166,66 @@ defmodule Farmbot.Firmware do
|
|||
def handle_info(:timeout, state) do
|
||||
case state.current do
|
||||
nil -> {:noreply, [], %{state | timer: nil}}
|
||||
%Current{fun: fun, args: args, from: from} = current ->
|
||||
%Current{fun: fun, args: args, from: _from} = current ->
|
||||
Logger.warn 1, "Got Firmware timeout. Retrying #{fun}(#{inspect args}) "
|
||||
case apply(state.handler_mod, fun, [state.handler | args]) do
|
||||
:ok ->
|
||||
timer = Process.send_after(self(), :timeout, state.timeout_ms)
|
||||
{:noreply, [], %{state | current: current, timer: timer}}
|
||||
{:error, _} = res ->
|
||||
GenStage.reply(from, res)
|
||||
do_reply(state, res)
|
||||
{:noreply, [], %{state | current: nil, queue: :queue.new()}}
|
||||
end
|
||||
{:noreply, [], %{state | timer: nil}}
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def handle_call({fun, _}, _from, state = %{initialized: false}) when fun not in [:read_all_params, :update_param, :emergency_unlock, :emergency_lock] do
|
||||
def handle_call({:fetch_param_hack_delete_me, param}, _, state) do
|
||||
{:reply, Map.get(state.params, param), [], state}
|
||||
end
|
||||
|
||||
def handle_call({fun, _}, _from, state = %{initialized: false}) when fun not in [:read_param, :read_all_params, :update_param, :emergency_unlock, :emergency_lock] do
|
||||
{:reply, {:error, :uninitialized}, [], state}
|
||||
end
|
||||
|
||||
def handle_call({fun, args}, from, state) do
|
||||
current = struct(Current, from: from, fun: fun, args: args)
|
||||
if :queue.is_empty(state.queue) do
|
||||
do_begin_cmd(current, state, [])
|
||||
next_current = struct(Current, from: from, fun: fun, args: args)
|
||||
current_current = state.current
|
||||
if current_current do
|
||||
do_queue_cmd(next_current, state)
|
||||
else
|
||||
do_queue_cmd(current, state)
|
||||
do_begin_cmd(next_current, state, [])
|
||||
end
|
||||
end
|
||||
|
||||
defp do_begin_cmd(%Current{fun: fun, args: args, from: _from} = current, state, dispatch) do
|
||||
# Logger.debug 3, "Firmware command: #{fun}#{inspect(args)}"
|
||||
|
||||
# Logger.busy 3, "FW Starting: #{fun}: #{inspect from}"
|
||||
case apply(state.handler_mod, fun, [state.handler | args]) do
|
||||
:ok ->
|
||||
if fun == :emergency_unlock, do: Farmbot.System.GPIO.Leds.led_status_ok()
|
||||
timer = Process.send_after(self(), :timeout, state.timeout_ms)
|
||||
{:noreply, dispatch, %{state | current: current, timer: timer}}
|
||||
{:error, _} = res ->
|
||||
{:reply, res, dispatch, %{state | current: nil, queue: :queue.new()}}
|
||||
do_reply(%{state | current: current}, res)
|
||||
{:noreply, dispatch, %{state | current: nil}}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_queue_cmd(%Current{fun: _fun, args: _args, from: _from} = current, state) do
|
||||
# Logger.busy 3, "FW Queuing: #{fun}: #{inspect from}"
|
||||
new_q = :queue.in(current, state.queue)
|
||||
{:noreply, [], %{state | queue: new_q}}
|
||||
end
|
||||
|
||||
def handle_events(gcodes, _from, state) do
|
||||
{diffs, state} = handle_gcodes(gcodes, state)
|
||||
if state.current == nil do
|
||||
# if after handling the current buffer of gcodes,
|
||||
# Try to start the next command in the queue if it exists.
|
||||
if List.last(gcodes) == :idle && state.current == nil do
|
||||
case :queue.out(state.queue) do
|
||||
{{:value, current}, new_queue} ->
|
||||
do_begin_cmd(current, %{state | queue: new_queue, current: current}, diffs)
|
||||
{:empty, queue} ->
|
||||
{{:value, next_current}, new_queue} ->
|
||||
do_begin_cmd(next_current, %{state | queue: new_queue, current: next_current}, diffs)
|
||||
{:empty, queue} -> # nothing to do if the queue is empty.
|
||||
{:noreply, diffs, %{state | queue: queue}}
|
||||
end
|
||||
else
|
||||
|
@ -242,7 +263,7 @@ defmodule Farmbot.Firmware do
|
|||
end
|
||||
end)
|
||||
Logger.error 1, "Failed to execute #{state.current.fun} #{inspect formatted_args}"
|
||||
GenStage.reply(state.current.from, {:error, :firmware_error})
|
||||
do_reply(state, {:error, :firmware_error})
|
||||
{nil, %{state | current: nil}}
|
||||
else
|
||||
{nil, state}
|
||||
|
@ -290,13 +311,17 @@ defmodule Farmbot.Firmware do
|
|||
|
||||
defp handle_gcode({:report_parameter_value, param, value}, state) when (value == -1) do
|
||||
Farmbot.System.ConfigStorage.update_config_value(:float, "hardware_params", to_string(param), nil)
|
||||
{:mcu_params, %{param => nil}, state}
|
||||
{:mcu_params, %{param => nil}, %{state | params: Map.put(state.params, param, value)}}
|
||||
end
|
||||
|
||||
defp handle_gcode({:report_parameter_value, param, value}, state) do
|
||||
defp handle_gcode({:report_parameter_value, param, value}, state) when is_number(value) do
|
||||
Farmbot.System.ConfigStorage.update_config_value(:float, "hardware_params", to_string(param), value / 1)
|
||||
{:mcu_params, %{param => value}, %{state | params: Map.put(state.params, param, value)}}
|
||||
end
|
||||
|
||||
{:mcu_params, %{param => value}, state}
|
||||
defp handle_gcode({:report_parameter_value, param, nil}, state) do
|
||||
Farmbot.System.ConfigStorage.update_config_value(:float, "hardware_params", to_string(param), nil)
|
||||
{:mcu_params, %{param => nil}, %{state | params: Map.put(state.params, param, nil)}}
|
||||
end
|
||||
|
||||
defp handle_gcode(:idle, %{initialized: false, initializing: false} = state) do
|
||||
|
@ -322,13 +347,13 @@ defmodule Farmbot.Firmware do
|
|||
Farmbot.BotState.set_busy(false)
|
||||
if state.current do
|
||||
# This might be a bug in the FW
|
||||
if state.current.command in [:home, :home_all] do
|
||||
if state.current.fun in [:home, :home_all] do
|
||||
Logger.warn 1, "Got idle during home. Ignoring. This might be bad."
|
||||
timer = Process.send_after(self(), :timeout, state.timeout_ms)
|
||||
{nil, %{state | timer: timer}}
|
||||
else
|
||||
Logger.warn 1, "Got idle while executing a command."
|
||||
GenStage.reply(state.current.from, {:error, :timeout})
|
||||
do_reply(state, {:error, :timeout})
|
||||
{:informational_settings, %{busy: false, locked: false}, %{state | current: nil, idle: true}}
|
||||
end
|
||||
else
|
||||
|
@ -379,7 +404,7 @@ defmodule Farmbot.Firmware do
|
|||
defp handle_gcode(:done, state) do
|
||||
maybe_cancel_timer(state.timer)
|
||||
if state.current do
|
||||
GenStage.reply(state.current.from, :ok)
|
||||
do_reply(state, :ok)
|
||||
{nil, %{state | current: nil}}
|
||||
else
|
||||
{nil, state}
|
||||
|
@ -389,7 +414,7 @@ defmodule Farmbot.Firmware do
|
|||
defp handle_gcode(:report_emergency_lock, state) do
|
||||
Farmbot.System.GPIO.Leds.led_status_err
|
||||
if state.current do
|
||||
GenStage.reply(state.current.from, {:error, :emergency_lock})
|
||||
do_reply(state, {:error, :emergency_lock})
|
||||
{:informational_settings, %{locked: true}, %{state | current: nil}}
|
||||
else
|
||||
{:informational_settings, %{locked: true}, state}
|
||||
|
@ -437,7 +462,31 @@ defmodule Farmbot.Firmware do
|
|||
|
||||
@doc false
|
||||
def do_read_params_and_report_position(old) when is_map(old) do
|
||||
for {key, float_val} <- old do
|
||||
if get_config_value(:bool, "settings", "fw_upgrade_migration") do
|
||||
Logger.busy(1, "Migrating old configuration data from firmware.")
|
||||
migration_hack = fn(param_atom) ->
|
||||
read_param(param_atom)
|
||||
val = GenServer.call(__MODULE__, {:fetch_param_hack_delete_me, param_atom})
|
||||
modified = cond do
|
||||
is_nil(val) -> 5556
|
||||
val == 56 -> 5556
|
||||
5556 -> 5556
|
||||
is_number(val) -> val * 100
|
||||
true -> 5556
|
||||
end
|
||||
Logger.info 2, "Update param: #{param_atom}: #{val} => #{inspect modified}"
|
||||
update_param(param_atom, modified)
|
||||
end
|
||||
for param <- [:encoder_scaling_x, :encoder_scaling_y, :encoder_scaling_z] do
|
||||
Logger.busy 3, "Migrating: #{inspect param}"
|
||||
migration_hack.(param)
|
||||
Logger.success 3, "Done Migrating: #{inspect param}"
|
||||
end
|
||||
update_config_value(:bool, "settings", "fw_upgrade_migration", false)
|
||||
Logger.success(1, "Finished migrating old configuration data.")
|
||||
end
|
||||
|
||||
for {key, float_val} <- Map.drop(old, ["encoder_scaling_x", "encoder_scaling_y", "encoder_scaling_z"]) do
|
||||
cond do
|
||||
(float_val == -1) -> :ok
|
||||
is_nil(float_val) -> :ok
|
||||
|
@ -471,4 +520,15 @@ defmodule Farmbot.Firmware do
|
|||
report_calibration_callback(tries - 1, param, val)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_reply(state, reply) do
|
||||
case state.current do
|
||||
%Current{fun: _fun, from: from} ->
|
||||
# Logger.success 3, "FW Replying: #{fun}: #{inspect from}"
|
||||
:ok = GenServer.reply from, reply
|
||||
nil ->
|
||||
Logger.error 1, "FW Nothing to send reply: #{inspect reply} to!."
|
||||
:error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,8 +14,8 @@ defmodule Farmbot.Firmware.StubHandler do
|
|||
GenStage.start_link(__MODULE__, [])
|
||||
end
|
||||
|
||||
def move_absolute(handler, pos, x_speed, y_speed, z_speed) do
|
||||
GenStage.call(handler, {:move_absolute, pos, x_speed, y_speed, z_speed})
|
||||
def move_absolute(handler, pos, x_spd, y_spd, z_spd) do
|
||||
GenStage.call(handler, {:move_absolute, pos, x_spd, y_spd, z_spd}, 120_000)
|
||||
end
|
||||
|
||||
def calibrate(handler, axis) do
|
||||
|
@ -116,6 +116,7 @@ defmodule Farmbot.Firmware.StubHandler do
|
|||
end
|
||||
|
||||
def handle_call({:move_absolute, pos, _x_speed, _y_speed, _z_speed}, _from, state) do
|
||||
Process.sleep(5000)
|
||||
response = build_resp [{:report_current_position, pos.x, pos.y, pos.z},
|
||||
{:report_encoder_position_scaled, pos.x, pos.y, pos.z},
|
||||
{:report_encoder_position_raw, pos.x, pos.y, pos.z}, :done]
|
||||
|
@ -202,7 +203,7 @@ defmodule Farmbot.Firmware.StubHandler do
|
|||
|
||||
def handle_call({:read_param, param}, _from, state) do
|
||||
res = state.fw_params[param]
|
||||
response = build_resp [{:report_paramater_value, param, res}, :done]
|
||||
response = build_resp [{:report_parameter_value, param, res}, :done]
|
||||
{:reply, build_reply(:ok), response, state}
|
||||
end
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ defmodule Farmbot.Logger.Console do
|
|||
:ok
|
||||
end
|
||||
|
||||
defp maybe_log(%Farmbot.Log{module: module} = log) do
|
||||
defp maybe_log(%Farmbot.Log{module: _module} = log) do
|
||||
# should_log = List.first(Module.split(module)) == "Farmbot"
|
||||
# if should_log do
|
||||
# credo:disable-for-next-line
|
||||
|
|
|
@ -8,7 +8,6 @@ defmodule Farmbot.Repo.FarmEvent do
|
|||
"""
|
||||
|
||||
@on_load :load_nif
|
||||
|
||||
def load_nif do
|
||||
require Logger
|
||||
nif_file = '#{:code.priv_dir(:farmbot)}/build_calendar'
|
||||
|
|
|
@ -26,24 +26,27 @@ defmodule Farmbot.Repo do
|
|||
# fifteen seconds.
|
||||
# @timeout 15000
|
||||
|
||||
# 1.5 minutes.
|
||||
@call_timeout_ms 90_000
|
||||
|
||||
@doc "Fetch the current repo."
|
||||
def current_repo do
|
||||
GenServer.call(__MODULE__, :current_repo, :infinity)
|
||||
GenServer.call(__MODULE__, :current_repo)
|
||||
end
|
||||
|
||||
@doc "Fetch the non current repo."
|
||||
def other_repo do
|
||||
GenServer.call(__MODULE__, :other_repo, :infinity)
|
||||
GenServer.call(__MODULE__, :other_repo)
|
||||
end
|
||||
|
||||
@doc "Flip the repos."
|
||||
def flip() do
|
||||
GenServer.call(__MODULE__, :flip, :infinity)
|
||||
def flip do
|
||||
GenServer.call(__MODULE__, :flip, @call_timeout_ms)
|
||||
end
|
||||
|
||||
@doc "Register a diff to be stored until a flip."
|
||||
def register_sync_cmd(remote_id, kind, body) do
|
||||
GenServer.call(__MODULE__, {:register_sync_cmd, remote_id, kind, body}, :infinity)
|
||||
GenServer.call(__MODULE__, {:register_sync_cmd, remote_id, kind, body})
|
||||
end
|
||||
|
||||
@doc false
|
||||
|
@ -56,6 +59,17 @@ defmodule Farmbot.Repo do
|
|||
GenServer.start_link(__MODULE__, repos, name: __MODULE__)
|
||||
end
|
||||
|
||||
defmodule State do
|
||||
@moduledoc false
|
||||
defstruct [
|
||||
:repos,
|
||||
:needs_hard_sync,
|
||||
:timer,
|
||||
:sync_pid,
|
||||
:from
|
||||
]
|
||||
end
|
||||
|
||||
def init([repo_a, repo_b]) do
|
||||
# Delete any old sync cmds.
|
||||
destroy_all_sync_cmds()
|
||||
|
@ -88,61 +102,98 @@ defmodule Farmbot.Repo do
|
|||
|
||||
# Copy configs
|
||||
[current, _] = repos
|
||||
copy_configs(current)
|
||||
{:ok, %{repos: repos, needs_hard_sync: needs_hard_sync, timer: start_timer()}}
|
||||
:ok = copy_configs(current)
|
||||
{:ok, %State{repos: repos, needs_hard_sync: needs_hard_sync, timer: start_timer(), sync_pid: nil}}
|
||||
end
|
||||
|
||||
def terminate(reason, _) do
|
||||
def terminate(reason, state) do
|
||||
if reason not in [:normal, :shutdown] do
|
||||
Logger.error 1, "Repo died: #{inspect reason}"
|
||||
BotState.set_sync_status(:sync_error)
|
||||
end
|
||||
|
||||
if state.from do
|
||||
GenServer.reply(state.from, reason)
|
||||
end
|
||||
Farmbot.FarmEvent.Manager.register_events([])
|
||||
end
|
||||
|
||||
def handle_info({:DOWN, _, :process, _, %State{} = new_state}, %State{} = state) do
|
||||
Logger.success(1, "Sync complete.")
|
||||
if state.from do
|
||||
GenServer.reply(state.from, :ok)
|
||||
end
|
||||
{:noreply, new_state}
|
||||
end
|
||||
|
||||
def handle_info({:DOWN, _, :process, _, reason}, state) do
|
||||
Logger.error 1, "Sync error: #{inspect reason}"
|
||||
if state.from do
|
||||
GenServer.reply(state.from, reason)
|
||||
end
|
||||
BotState.set_sync_status(:sync_now)
|
||||
destroy_all_sync_cmds()
|
||||
{:noreply, %State{state | sync_pid: nil, from: nil}}
|
||||
end
|
||||
|
||||
def handle_info(:timeout, state) do
|
||||
BotState.set_sync_status(:sync_now)
|
||||
destroy_all_sync_cmds()
|
||||
{:noreply, %State{state | timer: start_timer(), needs_hard_sync: true}}
|
||||
end
|
||||
|
||||
def handle_call(:force_hard_sync, _, state) do
|
||||
maybe_cancel_timer(state.timer)
|
||||
BotState.set_sync_status(:sync_now)
|
||||
Farmbot.FarmEvent.Manager.register_events([])
|
||||
{:reply, :ok, %{state | timer: nil, needs_hard_sync: true}}
|
||||
{:reply, :ok, %State{state | timer: nil, needs_hard_sync: true}}
|
||||
end
|
||||
|
||||
def handle_call(:current_repo, _, %{repos: [repo_a, _]} = state) do
|
||||
def handle_call(:current_repo, _, %State{repos: [repo_a, _]} = state) do
|
||||
{:reply, repo_a, state}
|
||||
end
|
||||
|
||||
def handle_call(:other_repo, _, %{repos: [_, repo_b]} = state) do
|
||||
def handle_call(:other_repo, _, %State{repos: [_, repo_b]} = state) do
|
||||
{:reply, repo_b, state}
|
||||
end
|
||||
|
||||
def handle_call(:flip, _, %{repos: [repo_a, repo_b], needs_hard_sync: true} = state) do
|
||||
maybe_cancel_timer(state.timer)
|
||||
destroy_all_sync_cmds()
|
||||
def handle_call(:flip, from, %State{repos: [repo_a, repo_b], needs_hard_sync: true} = state) do
|
||||
fun = fn() ->
|
||||
maybe_cancel_timer(state.timer)
|
||||
destroy_all_sync_cmds()
|
||||
BotState.set_sync_status(:syncing)
|
||||
do_sync_both(repo_a, repo_b)
|
||||
BotState.set_sync_status(:synced)
|
||||
:ok = copy_configs(repo_b)
|
||||
flip_repos_in_cs()
|
||||
exit(%State{state | repos: [repo_b, repo_a], needs_hard_sync: false, timer: start_timer(), sync_pid: nil})
|
||||
end
|
||||
Logger.busy(1, "Syncing.")
|
||||
BotState.set_sync_status(:syncing)
|
||||
do_sync_both(repo_a, repo_b)
|
||||
BotState.set_sync_status(:synced)
|
||||
copy_configs(repo_b)
|
||||
flip_repos_in_cs()
|
||||
Logger.success(1, "Sync complete.")
|
||||
{:reply, :ok, %{state | repos: [repo_b, repo_a], needs_hard_sync: false, timer: start_timer()}}
|
||||
pid = spawn(fun)
|
||||
Process.monitor(pid)
|
||||
{:noreply, %State{state | sync_pid: pid, from: from}}
|
||||
end
|
||||
|
||||
def handle_call(:flip, _, %{repos: [repo_a, repo_b]} = state) do
|
||||
maybe_cancel_timer(state.timer)
|
||||
def handle_call(:flip, from, %State{repos: [repo_a, repo_b]} = state) do
|
||||
fun = fn() ->
|
||||
maybe_cancel_timer(state.timer)
|
||||
BotState.set_sync_status(:syncing)
|
||||
|
||||
# Fetch all sync_cmds and apply them in order they were received.
|
||||
ConfigStorage.all(SyncCmd)
|
||||
|> Enum.sort(&Timex.before?(&1.inserted_at, &2.inserted_at))
|
||||
|> Enum.each(&apply_sync_cmd(repo_a, &1))
|
||||
|
||||
flip_repos_in_cs()
|
||||
BotState.set_sync_status(:synced)
|
||||
:ok = copy_configs(repo_b)
|
||||
destroy_all_sync_cmds()
|
||||
exit(%State{state | repos: [repo_b, repo_a], timer: start_timer(), sync_pid: nil})
|
||||
end
|
||||
Logger.busy(1, "Syncing.")
|
||||
BotState.set_sync_status(:syncing)
|
||||
|
||||
# Fetch all sync_cmds and apply them in order they were received.
|
||||
ConfigStorage.all(SyncCmd)
|
||||
|> Enum.sort(&Timex.before?(&1.inserted_at, &2.inserted_at))
|
||||
|> Enum.each(&apply_sync_cmd(repo_a, &1))
|
||||
|
||||
flip_repos_in_cs()
|
||||
BotState.set_sync_status(:synced)
|
||||
copy_configs(repo_b)
|
||||
destroy_all_sync_cmds()
|
||||
Logger.success(1, "Sync complete.")
|
||||
{:reply, repo_b, %{state | repos: [repo_b, repo_a], timer: start_timer()}}
|
||||
pid = spawn(fun)
|
||||
Process.monitor(pid)
|
||||
{:noreply, %State{state | sync_pid: pid, from: from}}
|
||||
end
|
||||
|
||||
def handle_call({:register_sync_cmd, remote_id, kind, body}, _from, state) do
|
||||
|
@ -152,28 +203,21 @@ defmodule Farmbot.Repo do
|
|||
case SyncCmd.changeset(struct(SyncCmd, %{remote_id: remote_id, kind: kind, body: body}))
|
||||
|> ConfigStorage.insert() do
|
||||
{:ok, sync_cmd} ->
|
||||
apply_sync_cmd(other_repo, sync_cmd)
|
||||
:ok = apply_sync_cmd(other_repo, sync_cmd)
|
||||
|
||||
case auto_sync?() do
|
||||
false -> BotState.set_sync_status(:sync_now)
|
||||
true -> BotState.set_sync_status(:syncing)
|
||||
false -> :ok = BotState.set_sync_status(:sync_now)
|
||||
true -> :ok = BotState.set_sync_status(:syncing)
|
||||
end
|
||||
|
||||
{:reply, :ok, %{state | timer: start_timer()}}
|
||||
{:reply, :ok, %State{state | timer: start_timer()}}
|
||||
|
||||
{:error, reason} ->
|
||||
BotState.set_sync_status(:sync_error)
|
||||
Logger.error(1, "Failed to apply sync command: #{inspect(reason)}")
|
||||
{:reply, :error, %{state | needs_hard_sync: true}}
|
||||
{:reply, :error, %State{state | needs_hard_sync: true}}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info(:timeout, state) do
|
||||
BotState.set_sync_status(:sync_now)
|
||||
destroy_all_sync_cmds()
|
||||
{:noreply, %{state | timer: start_timer(), needs_hard_sync: true}}
|
||||
end
|
||||
|
||||
defp copy_configs(repo) do
|
||||
case repo.one(Device) do
|
||||
nil ->
|
||||
|
@ -187,11 +231,12 @@ defmodule Farmbot.Repo do
|
|||
repo.all(Peripheral)
|
||||
|> Enum.all?(fn %{mode: mode, pin: pin} ->
|
||||
mode = if mode == 0, do: :digital, else: :analog
|
||||
# Logger.busy 3, "Reading peripheral (#{pin} - #{mode})"
|
||||
# Logger.busy 3, "Reading peripheral (#{pin} - #{mode})"
|
||||
Farmbot.Firmware.read_pin(pin, mode)
|
||||
end)
|
||||
|
||||
Farmbot.FarmEvent.Manager.register_events repo.all(Farmbot.Repo.FarmEvent)
|
||||
:ok
|
||||
end
|
||||
|
||||
defp destroy_all_sync_cmds do
|
||||
|
@ -282,12 +327,13 @@ defmodule Farmbot.Repo do
|
|||
nil ->
|
||||
mod.changeset(struct(mod), not_struct)
|
||||
|> repo.insert!
|
||||
|
||||
:ok
|
||||
# if there is an existing record, copy the ecto meta from the old
|
||||
# record. This allows `insert_or_update` to work properly.
|
||||
existing ->
|
||||
mod.changeset(existing, not_struct)
|
||||
|> repo.update!
|
||||
:ok
|
||||
end
|
||||
else
|
||||
Logger.warn(3, "Unknown module: #{mod} #{inspect(sync_cmd)}")
|
||||
|
|
|
@ -164,7 +164,7 @@ defmodule Farmbot.System.Updates.SlackUpdater do
|
|||
{'Authorization', 'Bearer #{state.token}'}
|
||||
]) do
|
||||
{:ok, path} ->
|
||||
Farmbot.System.Updates.apply_firmware(path)
|
||||
Farmbot.System.Updates.apply_firmware(path, true)
|
||||
{:stop, :normal, %{state | updating: true}}
|
||||
|
||||
{:error, reason} ->
|
||||
|
|
|
@ -19,13 +19,18 @@ defmodule Farmbot.System.UpdateTimer do
|
|||
GenServer.start_link(__MODULE__, [], [name: __MODULE__])
|
||||
end
|
||||
|
||||
def terminate(reason, _) do
|
||||
Logger.error 1, "Failed to check updates: #{inspect reason}"
|
||||
end
|
||||
|
||||
def init([]) do
|
||||
spawn __MODULE__, :wait_for_http, [self()]
|
||||
{:ok, [], :hibernate}
|
||||
end
|
||||
|
||||
def handle_info(:checkup, state) do
|
||||
Farmbot.System.Updates.check_updates()
|
||||
osau = Farmbot.System.ConfigStorage.get_config_value(:bool, "settings", "os_auto_update")
|
||||
Farmbot.System.Updates.check_updates(osau)
|
||||
Process.send_after(self(), :checkup, @twelve_hours)
|
||||
{:noreply, state, :hibernate}
|
||||
end
|
||||
|
|
|
@ -5,6 +5,7 @@ defmodule Farmbot.System.Updates do
|
|||
@data_path Application.get_env(:farmbot, :data_path)
|
||||
@current_version Farmbot.Project.version()
|
||||
@target Farmbot.Project.target()
|
||||
@current_commit Farmbot.Project.commit()
|
||||
@env Farmbot.Project.env()
|
||||
use Farmbot.Logger
|
||||
|
||||
|
@ -13,13 +14,20 @@ defmodule Farmbot.System.Updates do
|
|||
|
||||
alias Farmbot.System.ConfigStorage
|
||||
|
||||
@doc "Overwrite os update server field"
|
||||
def override_update_server(url) do
|
||||
ConfigStorage.update_config_value(:bool, "settings", "beta_opt_in", true)
|
||||
ConfigStorage.update_config_value(:string, "settings", "os_update_server_overwrite", url)
|
||||
end
|
||||
|
||||
@doc "Force check updates."
|
||||
def check_updates do
|
||||
def check_updates(reboot) do
|
||||
token = ConfigStorage.get_config_value(:string, "authorization", "token")
|
||||
if token do
|
||||
case Farmbot.Jwt.decode(token) do
|
||||
{:ok, %{os_update_server: update_server}} ->
|
||||
do_check_updates_http(update_server)
|
||||
{:ok, %Farmbot.Jwt{os_update_server: update_server}} ->
|
||||
override = ConfigStorage.get_config_value(:string, "settings", "os_update_server_overwrite")
|
||||
do_check_updates_http(override || update_server, reboot)
|
||||
_ -> no_token()
|
||||
end
|
||||
else
|
||||
|
@ -32,28 +40,43 @@ defmodule Farmbot.System.Updates do
|
|||
:ok
|
||||
end
|
||||
|
||||
defp do_check_updates_http(url) do
|
||||
defp do_check_updates_http(url, reboot) do
|
||||
Logger.info 3, "Checking: #{url} for updates."
|
||||
with {:ok, %{body: body, status_code: 200}} <- Farmbot.HTTP.get(url),
|
||||
{:ok, data} <- Poison.decode(body),
|
||||
{:ok, prerelease} <- Map.fetch(data, "prerelease"),
|
||||
{:ok, new_commit} <- Map.fetch(data, "target_commitish"),
|
||||
{:ok, cl} <- Map.fetch(data, "body"),
|
||||
{:ok, false} <- Map.fetch(data, "draft"),
|
||||
{:ok, "v" <> new_version_str} <- Map.fetch(data, "tag_name"),
|
||||
{:ok, new_version} <- Version.parse(new_version_str),
|
||||
{:ok, current_version} <- Version.parse(@current_version),
|
||||
{:ok, fw_url} <- find_fw_url(data, new_version) do
|
||||
needs_update = case Version.compare(current_version, new_version) do
|
||||
s when s in [:gt, :eq] ->
|
||||
Logger.success 2, "Farmbot is up to date."
|
||||
false
|
||||
:lt ->
|
||||
Logger.busy 1, "New Farmbot firmware update: #{new_version}"
|
||||
true
|
||||
needs_update = if prerelease do
|
||||
val = new_commit == @current_commit
|
||||
Logger.info 1, "Checking prerelease commits: current_commit: #{@current_commit} new_commit: #{new_commit} #{val}"
|
||||
!val
|
||||
else
|
||||
case Version.compare(current_version, new_version) do
|
||||
s when s in [:gt, :eq] ->
|
||||
Logger.success 2, "Farmbot is up to date."
|
||||
false
|
||||
:lt ->
|
||||
Logger.busy 1, "New Farmbot firmware update: #{new_version}"
|
||||
true
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
if should_apply_update(@env, prerelease, needs_update) do
|
||||
Logger.info 1, "Downloading update. Here is the release notes"
|
||||
Logger.info 1, cl
|
||||
do_download_and_apply(fw_url, new_version)
|
||||
Logger.busy 1, "Downloading FarmbotOS over the air update"
|
||||
IO.puts cl
|
||||
# Logger.info 1, "Downloading update. Here is the release notes"
|
||||
# Logger.info 1, cl
|
||||
do_download_and_apply(fw_url, new_version, reboot)
|
||||
else
|
||||
:no_update
|
||||
end
|
||||
else
|
||||
:error ->
|
||||
|
@ -71,7 +94,7 @@ defmodule Farmbot.System.Updates do
|
|||
{:ok, res} -> res
|
||||
_ -> body
|
||||
end
|
||||
Logger.error 1, "HTTP error: #{code}: #{reason}"
|
||||
Logger.error 1, "OS Update HTTP error: #{code}: #{inspect reason}"
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -94,30 +117,38 @@ defmodule Farmbot.System.Updates do
|
|||
defp should_apply_update(env, prerelease?, needs_update?)
|
||||
defp should_apply_update(_, _, false), do: false
|
||||
defp should_apply_update(:prod, true, _) do
|
||||
Logger.info 3, "Not applying prerelease firmware."
|
||||
false
|
||||
if ConfigStorage.get_config_value(:bool, "settings", "beta_opt_in") do
|
||||
Logger.info 3, "Applying beta update for production firmware"
|
||||
true
|
||||
else
|
||||
Logger.info 3, "Not applying prerelease update for production firmware"
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
defp should_apply_update(_env, true, _) do
|
||||
Logger.info 3, "Applying prerelease firmware."
|
||||
true
|
||||
end
|
||||
|
||||
defp should_apply_update(_, _, true) do
|
||||
true
|
||||
end
|
||||
|
||||
defp do_download_and_apply(dl_url, new_version) do
|
||||
defp do_download_and_apply(dl_url, new_version, reboot) do
|
||||
dl_fun = Farmbot.BotState.download_progress_fun("FBOS_OTA")
|
||||
dl_path = Path.join(@data_path, "#{new_version}.fw")
|
||||
case Farmbot.HTTP.download_file(dl_url, dl_path, dl_fun, "", []) do
|
||||
{:ok, path} ->
|
||||
apply_firmware(path)
|
||||
apply_firmware(path, reboot)
|
||||
{:error, reason} ->
|
||||
Logger.error 1, "Failed to download update file: #{inspect reason}"
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Apply an OS (fwup) firmware."
|
||||
def apply_firmware(file_path, reboot \\ false) do
|
||||
def apply_firmware(file_path, reboot) do
|
||||
Logger.busy 1, "Applying #{@target} OS update"
|
||||
before_update()
|
||||
case @handler.apply_firmware(file_path) do
|
||||
|
@ -129,6 +160,7 @@ defmodule Farmbot.System.Updates do
|
|||
end
|
||||
{:error, reason} ->
|
||||
Logger.error 1, "Failed to apply update: #{inspect reason}"
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -37,6 +37,48 @@ defmodule Mix.Tasks.Farmbot.Env do
|
|||
to_string(Farmbot.Project.env())
|
||||
end
|
||||
|
||||
@doc false
|
||||
def format_date_time(%{ctime: {{yr, m, day}, {hr, min, sec}}}) do
|
||||
dt =
|
||||
%DateTime{
|
||||
hour: hr,
|
||||
year: yr,
|
||||
month: m,
|
||||
day: day,
|
||||
minute: min,
|
||||
second: sec,
|
||||
time_zone: "Etc/UTC",
|
||||
zone_abbr: "UTC",
|
||||
std_offset: 0,
|
||||
utc_offset: 0
|
||||
}
|
||||
|> Timex.local()
|
||||
|
||||
"#{dt.year}-#{pad(dt.month)}-#{pad(dt.day)}_#{pad(dt.hour)}#{pad(dt.minute)}"
|
||||
end
|
||||
|
||||
defp pad(int) do
|
||||
if int < 10, do: "0#{int}", else: "#{int}"
|
||||
end
|
||||
|
||||
@doc false
|
||||
def build_comment(time, comment) do
|
||||
"""
|
||||
*New Farmbot Firmware!*
|
||||
> *_Env_*: `#{env()}`
|
||||
> *_Target_*: `#{target()}`
|
||||
> *_Version_*: `#{mix_config(:version)}`
|
||||
> *_Commit_*: `#{mix_config(:commit)}`
|
||||
> *_Time_*: `#{time}`
|
||||
#{commit_message()}
|
||||
#{String.trim(comment)}
|
||||
"""
|
||||
end
|
||||
|
||||
defp commit_message do
|
||||
System.cmd("git", ~w(log -1 --pretty=%B)) |> elem(0) |> String.trim()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def slack_token do
|
||||
System.get_env("SLACK_TOKEN") || Mix.raise "No $SLACK_TOKEN environment variable."
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
defmodule Mix.Tasks.Farmbot.ProdImage do
|
||||
@moduledoc "Build a production firmware image file"
|
||||
@shortdoc @moduledoc
|
||||
|
||||
use Mix.Task
|
||||
import Mix.Tasks.Farmbot.Env
|
||||
|
||||
def run(opts) do
|
||||
{keywords, _, _} =
|
||||
opts |> OptionParser.parse(switches: [signed: :boolean])
|
||||
|
||||
signed? = Keyword.get(keywords, :signed, false)
|
||||
Application.ensure_all_started(:timex)
|
||||
|
||||
fw_file_to_upload = if signed?, do: signed_fw_file(), else: fw_file()
|
||||
time = format_date_time(File.stat!(fw_file_to_upload))
|
||||
|
||||
filename =
|
||||
"#{mix_config(:app)}-#{target()}-#{env()}-#{mix_config(:commit)}#{
|
||||
if signed?, do: "-signed-", else: "-"
|
||||
}#{time}.img"
|
||||
Mix.shell().info(build_comment(time, ""))
|
||||
Mix.Tasks.Firmware.Image.run([filename])
|
||||
end
|
||||
end
|
|
@ -54,51 +54,7 @@ defmodule Mix.Tasks.Farmbot.Firmware.Slack do
|
|||
end
|
||||
end
|
||||
|
||||
defp build_comment(time, comment) do
|
||||
# %{
|
||||
# env: env(),
|
||||
# target: target(),
|
||||
# version: mix_config(:version),
|
||||
# commit: mix_config(:commit),
|
||||
# time: time,
|
||||
# comment: comment
|
||||
# } |> Poison.encode!(pretty: true)
|
||||
|
||||
"""
|
||||
*New Farmbot Firmware!*
|
||||
> *_Env_*: `#{env()}`
|
||||
> *_Target_*: `#{target()}`
|
||||
> *_Version_*: `#{mix_config(:version)}`
|
||||
> *_Commit_*: `#{mix_config(:commit)}`
|
||||
> *_Time_*: `#{time}`
|
||||
#{String.trim(comment)}
|
||||
"""
|
||||
end
|
||||
|
||||
defp error(msg) do
|
||||
Mix.raise("Upload failed! " <> msg)
|
||||
end
|
||||
|
||||
defp format_date_time(%{ctime: {{yr, m, day}, {hr, min, sec}}}) do
|
||||
dt =
|
||||
%DateTime{
|
||||
hour: hr,
|
||||
year: yr,
|
||||
month: m,
|
||||
day: day,
|
||||
minute: min,
|
||||
second: sec,
|
||||
time_zone: "Etc/UTC",
|
||||
zone_abbr: "UTC",
|
||||
std_offset: 0,
|
||||
utc_offset: 0
|
||||
}
|
||||
|> Timex.local()
|
||||
|
||||
"#{dt.year}-#{pad(dt.month)}-#{pad(dt.day)}_#{pad(dt.hour)}#{pad(dt.minute)}"
|
||||
end
|
||||
|
||||
defp pad(int) do
|
||||
if int < 10, do: "0#{int}", else: "#{int}"
|
||||
end
|
||||
end
|
||||
|
|
13
mix.exs
13
mix.exs
|
@ -4,8 +4,7 @@ defmodule Farmbot.Mixfile do
|
|||
@version Path.join(__DIR__, "VERSION") |> File.read!() |> String.trim()
|
||||
|
||||
defp commit() do
|
||||
{t, _} = System.cmd("git", ["log", "--pretty=format:%h", "-1"])
|
||||
t
|
||||
System.cmd("git", ~w"rev-parse --verify HEAD") |> elem(0) |> String.trim()
|
||||
end
|
||||
|
||||
Mix.shell().info([
|
||||
|
@ -23,7 +22,7 @@ defmodule Farmbot.Mixfile do
|
|||
app: :farmbot,
|
||||
description: "The Brains of the Farmbot Project",
|
||||
package: package(),
|
||||
compilers: [:elixir_make] ++ Mix.compilers,
|
||||
compilers: compilers(),
|
||||
make_clean: ["clean"],
|
||||
test_coverage: [tool: ExCoveralls],
|
||||
version: @version,
|
||||
|
@ -75,6 +74,14 @@ defmodule Farmbot.Mixfile do
|
|||
]
|
||||
end
|
||||
|
||||
defp compilers do
|
||||
case :init.get_plain_arguments() |> List.last() do
|
||||
a when a in ['mix', 'compile', 'firmware'] ->
|
||||
[:elixir_make] ++ Mix.compilers
|
||||
_ -> Mix.compilers
|
||||
end
|
||||
end
|
||||
|
||||
defp deps do
|
||||
[
|
||||
{:nerves, "~> 0.8.3", runtime: false},
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,10 @@
|
|||
defmodule Farmbot.System.ConfigStorage.Migrations.AddBetaChanelConfig do
|
||||
use Ecto.Migration
|
||||
|
||||
import Farmbot.System.ConfigStorage.MigrationHelpers
|
||||
|
||||
def change do
|
||||
create_settings_config("beta_opt_in", :bool, false)
|
||||
create_settings_config("os_update_server_overwrite", :string, nil)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
defmodule Farmbot.System.ConfigStorage.Migrations.AddSpecialFwMigrationConfig do
|
||||
use Ecto.Migration
|
||||
|
||||
import Farmbot.System.ConfigStorage.MigrationHelpers
|
||||
|
||||
def change do
|
||||
create_settings_config("fw_upgrade_migration", :bool, true)
|
||||
end
|
||||
end
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue