Nerves Hub (#647)

pull/667/head
Connor Rigby 2018-11-21 16:20:11 -08:00
parent 15ad5dc4ab
commit a23d223995
44 changed files with 8901 additions and 896 deletions

View File

@ -1,7 +1,7 @@
version: 2.0
defaults: &defaults
docker:
- image: nervesproject/nerves_system_br:1.4.2
- image: nervesproject/nerves_system_br:1.5.2
install_elixir: &install_elixir
run:
@ -38,136 +38,182 @@ install_slack_helpers: &install_slack_helpers
command: |
wget https://gist.githubusercontent.com/ConnorRigby/03e722be4be70f8588f5ed74420e4eaa/raw/28a51d8f52ec7d569e8f7f20b83349816ddf63cf/slack_message.ex
install_ghr: &install_ghr
run:
name: Install ghr (Github Releases)
command: |
wget https://github.com/tcnksm/ghr/releases/download/v0.9.0/ghr_v0.9.0_linux_amd64.tar.gz
tar xf ghr_v0.9.0_linux_amd64.tar.gz
ln -sf ghr_v0.9.0_linux_amd64/ghr .
install_jq: &install_jq
run:
name: Install jq
command: |
wget https://github.com/stedolan/jq/releases/download/jq-1.5/jq-linux64
chmod +x ./jq-linux64
jobs:
test:
<<: *defaults
environment:
MIX_ENV: test
MIX_TARGET: host
NERVES_LOG_DISABLE_PROGRESS_BAR: "yes"
ELIXIR_VERSION: 1.7.3
SKIP_ARDUINO_BUILD: 1
steps:
- checkout
- run: git submodule update --init --recursive
- restore_cache:
key: v7-fbos-host-test-dependency-cache-{{ checksum "mix.lock.host" }}
- <<: *install_elixir
- restore_cache:
keys:
- v6-arduino-cache-{{ checksum ".circleci/setup-arduino.sh" }}
- <<: *install_arduino
- <<: *install_hex_archives
- restore_cache:
keys:
- v6-dep-cache-{{ checksum "mix.lock.host" }}
- <<: *fetch_and_compile_deps
- run:
name: Test Farmbot OS
command: |
mix deps.get
mix compile
- save_cache:
key: v6-dep-cache-{{ checksum "mix.lock.host" }}
key: v7-fbos-host-test-dependency-cache-{{ checksum "mix.lock.host" }}
paths:
- _build/host
- _build/arduino
- deps/host
- save_cache:
key: v6-arduino-cache-{{ checksum ".circleci/setup-arduino.sh" }}
paths:
- ~/arduino-1.8.5
- run:
name: "Credo code static code analysis"
command: mix credo
- run:
name: "Elixir Code formatter"
command: mix format --check-formatted
- run:
command: mix coveralls.circle --exclude farmbot_firmware
firmware_dev:
build_rpi3_prod_firmware:
<<: *defaults
environment:
MIX_TARGET: rpi3
MIX_ENV: dev
ENV: CI
NERVES_LOG_DISABLE_PROGRESS_BAR: "yes"
steps:
- checkout
- run: git submodule update --init --recursive
- <<: *install_elixir
- restore_cache:
keys:
- v6-arduino-cache-{{ checksum ".circleci/setup-arduino.sh" }}
- <<: *install_arduino
- restore_cache:
keys:
- v6-dependency-cache-{{ checksum "mix.lock.rpi3" }}
- <<: *install_hex_archives
- <<: *fetch_and_compile_deps
- run: mix firmware
- save_cache:
key: v6-dependency-cache-{{ checksum "mix.lock.rpi3" }}
paths:
- _build/rpi3
- _build/host
- _build/arduino
- deps/rpi3
- deps/host
- ~/.nerves
- run: mkdir -p artifacts
- run:
name: Decode fwup priv key
command: echo $FWUP_KEY_BASE64 | base64 --decode --ignore-garbage > $NERVES_FW_PRIV_KEY
- run:
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: v6-firmware-{{ .Revision }}-{{ .Environment.CIRCLE_SHA1 }}
paths:
- ./artifacts
firmware_beta:
<<: *defaults
environment:
MIX_TARGET: rpi3
MIX_ENV: prod
ENV: CI
NERVES_LOG_DISABLE_PROGRESS_BAR: "yes"
MIX_TARGET: rpi3
ELIXIR_VERSION: 1.7.3
SKIP_ARDUINO_BUILD: 1
steps:
- checkout
- run: git submodule update --init --recursive
- restore_cache:
key: v7-fbos-rpi3-prod-dependency-cache-{{ checksum "mix.lock.rpi3" }}
- restore_cache:
key: v7-fbos-host-test-dependency-cache-{{ checksum "mix.lock.host" }}
- <<: *install_elixir
- restore_cache:
keys:
- v6-arduino-cache-{{ checksum ".circleci/setup-arduino.sh" }}
- <<: *install_arduino
- <<: *install_hex_archives
- restore_cache:
keys:
- v6-dependency-cache-{{ checksum "mix.lock.rpi3" }}
- run: MIX_ENV=prod MIX_TARGET=rpi3 mix deps.get
- run: MIX_ENV=prod MIX_TARGET=rpi3 mix compile
- run: MIX_ENV=prod MIX_TARGET=rpi3 mix firmware
- run:
name: Build Farmbot OS Firmware
command: |
mix deps.get
mix compile --force
mix firmware
- run:
name: Create artifact dir
command: mkdir -p /nerves/deploy/system/artifacts
- run:
name: Create artifacts
command: |
cp _build/rpi3/prod/nerves/images/farmbot.fw /nerves/deploy/system/artifacts/farmbot-${MIX_TARGET}-$(cat VERSION).fw
- save_cache:
key: v6-dependency-cache-{{ checksum "mix.lock.rpi3" }}
key: v7-fbos-rpi3-prod-dependency-cache-{{ checksum "mix.lock.rpi3" }}
paths:
- _build/rpi3
- _build/host
- _build/arduino
- _build/rpi3/
- deps/rpi3
- deps/host
- ~/.nerves
- run: mkdir -p artifacts
- run:
name: Decode fwup priv key
command: echo $FWUP_KEY_BASE64 | base64 --decode --ignore-garbage > $NERVES_FW_PRIV_KEY
- run:
name: Sign firmware
command: MIX_ENV=prod MIX_TARGET=rpi3 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
- ~/.nerves/
- store_artifacts:
path: /nerves/deploy/system/artifacts
destination: images
- save_cache:
key: v6-firmware-{{ .Revision }}-{{ .Environment.CIRCLE_SHA1 }}
key: nerves/deploy/system-{{ .Revision }}-{{ .Environment.CIRCLE_TAG }}
paths:
- ./artifacts
deploy_beta_firmware:
- "/nerves/deploy/system"
deploy_rpi3_prod_firmware_master:
<<: *defaults
environment:
MIX_ENV: prod
MIX_TARGET: rpi3
ELIXIR_VERSION: 1.7.3
SKIP_ARDUINO_BUILD: 1
steps:
- checkout
- run: git submodule update --init --recursive
- restore_cache:
key: v7-fbos-rpi3-prod-dependency-cache-{{ checksum "mix.lock.rpi3" }}
- restore_cache:
key: v7-fbos-host-test-dependency-cache-{{ checksum "mix.lock.host" }}
- restore_cache:
key: nerves/deploy/system-{{ .Revision }}-{{ .Environment.CIRCLE_TAG }}
- <<: *install_elixir
- <<: *install_hex_archives
- <<: *install_ghr
- run:
name: Sign Image
command: mix nerves_hub.firmware sign --key prod /nerves/deploy/system/artifacts/farmbot-${MIX_TARGET}-$(cat VERSION).fw
- run:
name: Publish to NervesHub
command: mix nerves_hub.firmware publish --deploy prod-stable /nerves/deploy/system/artifacts/farmbot-${MIX_TARGET}-$(cat VERSION).fw
deploy_rpi3_prod_firmware_beta:
<<: *defaults
environment:
MIX_ENV: prod
MIX_TARGET: rpi3
ELIXIR_VERSION: 1.7.3
SKIP_ARDUINO_BUILD: 1
steps:
- checkout
- run: git submodule update --init --recursive
- restore_cache:
key: v7-fbos-rpi3-prod-dependency-cache-{{ checksum "mix.lock.rpi3" }}
- restore_cache:
key: v7-fbos-host-test-dependency-cache-{{ checksum "mix.lock.host" }}
- restore_cache:
key: nerves/deploy/system-{{ .Revision }}-{{ .Environment.CIRCLE_TAG }}
- <<: *install_elixir
- <<: *install_hex_archives
- run:
name: Sign Image
command: mix nerves_hub.firmware sign --key prod /nerves/deploy/system/artifacts/farmbot-${MIX_TARGET}-$(cat VERSION).fw
- run:
name: Publish to NervesHub
command: mix nerves_hub.firmware publish --deploy prod-beta /nerves/deploy/system/artifacts/farmbot-${MIX_TARGET}-$(cat VERSION).fw
deploy_rpi3_prod_firmware_staging:
<<: *defaults
environment:
MIX_ENV: prod
MIX_TARGET: rpi3
ELIXIR_VERSION: 1.7.3
SKIP_ARDUINO_BUILD: 1
steps:
- checkout
- run: git submodule update --init --recursive
- restore_cache:
key: v7-fbos-rpi3-prod-dependency-cache-{{ checksum "mix.lock.rpi3" }}
- restore_cache:
key: v7-fbos-host-test-dependency-cache-{{ checksum "mix.lock.host" }}
- restore_cache:
key: nerves/deploy/system-{{ .Revision }}-{{ .Environment.CIRCLE_TAG }}
- <<: *install_elixir
- <<: *install_hex_archives
- run:
name: Sign Image
command: mix nerves_hub.firmware sign --key staging /nerves/deploy/system/artifacts/farmbot-${MIX_TARGET}-$(cat VERSION).fw
- run:
name: Publish to NervesHub
command: mix nerves_hub.firmware publish --deploy prod-staging --ttl 3600 /nerves/deploy/system/artifacts/farmbot-${MIX_TARGET}-$(cat VERSION).fw
publish_rpi3_prod_firmware_beta_release:
<<: *defaults
environment:
MIX_ENV: prod
MIX_TARGET: rpi3
ELIXIR_VERSION: 1.7.3
SKIP_ARDUINO_BUILD: 1
steps:
- checkout
- run: git submodule update --init --recursive
- restore_cache:
key: v7-fbos-rpi3-prod-dependency-cache-{{ checksum "mix.lock.rpi3" }}
- restore_cache:
key: v7-fbos-host-test-dependency-cache-{{ checksum "mix.lock.host" }}
- restore_cache:
key: nerves/deploy/system-{{ .Revision }}-{{ .Environment.CIRCLE_TAG }}
- <<: *install_elixir
- <<: *install_hex_archives
- <<: *install_ghr
- <<: *install_slack_helpers
- <<: *install_jq
- run:
name: Run setup script
command: bash .circleci/setup-heroku.sh
@ -175,18 +221,16 @@ jobs:
fingerprints:
- "97:92:32:5d:d7:96:e1:fa:f3:6b:f3:bd:d6:aa:84:c6"
- run:
name: Install dependencies
name: Sign Firmware
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
echo $FWUP_KEY_BASE64 | base64 --decode --ignore-garbage > $NERVES_FW_PRIV_KEY
mv /nerves/deploy/system/artifacts/farmbot-${MIX_TARGET}-$(cat VERSION).fw /tmp/farmbot-${MIX_TARGET}-$(cat VERSION).fw
fwup -S -s $NERVES_FW_PRIV_KEY -i /tmp/farmbot-${MIX_TARGET}-$(cat VERSION).fw -o /nerves/deploy/system/artifacts/farmbot-${MIX_TARGET}-$(cat VERSION)-beta.fw
- run:
command: grep -Pazo "(?s)(?<=# $(cat VERSION))[^#]+" CHANGELOG.md > RELEASE_NOTES
- restore_cache:
key: v6-firmware-{{ .Revision }}-{{ .Environment.CIRCLE_SHA1 }}
- 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
name: Publish Github Release
command: ./ghr -t $GITHUB_TOKEN -u farmbot -r farmbot_os -prerelease -recreate -prerelease -b "$(cat RELEASE_NOTES)" -c $(git rev-parse --verify HEAD) "v$(cat VERSION)-beta" /nerves/deploy/system/artifacts/
- run:
name: Update heroku env
command: |
@ -195,122 +239,171 @@ jobs:
heroku config:set BETA_OTA_URL=$OTA_URL --app=farmbot-production
heroku config:set BETA_OTA_URL=$OTA_URL --app=farmbot-staging
elixir slack_message.ex $SLACK_MESSAGE
firmware_prod:
publish_rpi3_prod_firmware_master_release:
<<: *defaults
environment:
MIX_TARGET: rpi3
MIX_ENV: prod
ENV: CI
NERVES_LOG_DISABLE_PROGRESS_BAR: "yes"
MIX_TARGET: rpi3
ELIXIR_VERSION: 1.7.3
SKIP_ARDUINO_BUILD: 1
steps:
- checkout
- run: git submodule update --init --recursive
- restore_cache:
key: v7-fbos-rpi3-prod-dependency-cache-{{ checksum "mix.lock.rpi3" }}
- restore_cache:
key: v7-fbos-host-test-dependency-cache-{{ checksum "mix.lock.host" }}
- restore_cache:
key: nerves/deploy/system-{{ .Revision }}-{{ .Environment.CIRCLE_TAG }}
- <<: *install_elixir
- restore_cache:
keys:
- v6-arduino-cache-{{ checksum ".circleci/setup-arduino.sh" }}
- <<: *install_arduino
- <<: *install_hex_archives
- restore_cache:
keys:
- v6-dependency-cache-{{ checksum "mix.lock.rpi3" }}
- run: mix deps.get
- run: mix compile
- run: mix firmware
- save_cache:
key: v6-dependency-cache-{{ checksum "mix.lock.rpi3" }}
paths:
- _build/rpi3
- _build/host
- _build/arduino
- deps/rpi3
- deps/host
- ~/.nerves
- run: mkdir -p artifacts
- <<: *install_ghr
- <<: *install_slack_helpers
- run:
name: Decode fwup priv key
command: echo $FWUP_KEY_BASE64 | base64 --decode --ignore-garbage > $NERVES_FW_PRIV_KEY
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: 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).fw
- run:
name: Create img
command: mix firmware.image artifacts/farmbot-${MIX_TARGET}-$(cat VERSION).img
- save_cache:
key: v6-firmware-{{ .Revision }}-{{ .Environment.CIRCLE_SHA1 }}
paths:
- ./artifacts
deploy_prod_firmware:
<<: *defaults
steps:
- checkout
- run:
name: Install dependencies
name: Sign Firmware
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
echo $FWUP_KEY_BASE64 | base64 --decode --ignore-garbage > $NERVES_FW_PRIV_KEY
mv /nerves/deploy/system/artifacts/farmbot-${MIX_TARGET}-$(cat VERSION).fw /tmp/farmbot-${MIX_TARGET}-$(cat VERSION).fw
fwup -S -s $NERVES_FW_PRIV_KEY -i /tmp/farmbot-${MIX_TARGET}-$(cat VERSION).fw -o /nerves/deploy/system/artifacts/farmbot-${MIX_TARGET}-$(cat VERSION).fw
- run:
name: Create Image file
command: fwup -a -t complete -i /nerves/deploy/system/artifacts/farmbot-${MIX_TARGET}-$(cat VERSION).fw -d /nerves/deploy/system/farmbot-${MIX_TARGET}-$(cat VERSION).img
- run:
command: grep -Pazo "(?s)(?<=# $(cat VERSION))[^#]+" CHANGELOG.md > RELEASE_NOTES
- restore_cache:
key: v6-firmware-{{ .Revision }}-{{ .Environment.CIRCLE_SHA1 }}
- run:
command: ./ghr -t $GITHUB_TOKEN -u farmbot -r farmbot_os -recreate -prerelease -draft -delete -b "$(cat RELEASE_NOTES)" -c $(git rev-parse --verify HEAD) "v$(cat VERSION)" $PWD/artifacts
name: Publish Github Release
command: ./ghr -t $GITHUB_TOKEN -u farmbot -r farmbot_os -recreate -prerelease -draft -delete -b "$(cat RELEASE_NOTES)" -c $(git rev-parse --verify HEAD) "v$(cat VERSION)" /nerves/deploy/system/
- run:
name: Send Slack Message
command: elixir slack_message.ex "New Farmbot Prod release $(cat VERSION)"
workflows:
version: 2
test_firmware_upload:
test:
jobs:
- test:
context: org-global
filters:
branches:
ignore:
# Merging is blocked on these branches until tests pass.
- beta
- master
- firmware_dev:
context: org-global
requires:
- test
filters:
branches:
ignore:
- beta
- master
deploy_beta:
- staging
deploy_stable_production:
jobs:
- firmware_beta:
context: org-global
- build_rpi3_prod_firmware:
context: farmbot-production
filters:
branches:
only:
- beta
- deploy_beta_firmware:
context: org-global
- master
- deploy_rpi3_prod_firmware_master:
context: farmbot-production
filters:
branches:
only:
- beta
- master
requires:
- firmware_beta
deploy_prod:
- build_rpi3_prod_firmware
- publish_rpi3_prod_firmware_master_release:
context: org-global
filters:
branches:
only:
- master
requires:
- build_rpi3_prod_firmware
deploy_stable_staging:
jobs:
- firmware_prod:
context: org-global
- build_rpi3_prod_firmware:
context: farmbot-staging
filters:
branches:
only:
- master
- deploy_prod_firmware:
context: org-global
- deploy_rpi3_prod_firmware_master:
context: farmbot-staging
filters:
branches:
only:
- master
requires:
- firmware_prod
- build_rpi3_prod_firmware
deploy_beta_production:
jobs:
- build_rpi3_prod_firmware:
context: farmbot-production
filters:
branches:
only:
- beta
- deploy_rpi3_prod_firmware_beta:
context: farmbot-production
filters:
branches:
only:
- beta
requires:
- build_rpi3_prod_firmware
- publish_rpi3_prod_firmware_beta_release:
context: org-global
filters:
branches:
only:
- beta
requires:
- build_rpi3_prod_firmware
deploy_beta_staging:
jobs:
- build_rpi3_prod_firmware:
context: farmbot-staging
filters:
branches:
only:
- beta
- deploy_rpi3_prod_firmware_beta:
context: farmbot-staging
filters:
branches:
only:
- beta
requires:
- build_rpi3_prod_firmware
deploy_staging_production:
jobs:
- build_rpi3_prod_firmware:
context: farmbot-production
filters:
branches:
only:
- staging
- deploy_rpi3_prod_firmware_staging:
context: farmbot-production
filters:
branches:
only:
- staging
requires:
- build_rpi3_prod_firmware
deploy_staging_staging:
jobs:
- build_rpi3_prod_firmware:
context: farmbot-staging
filters:
branches:
only:
- staging
- deploy_rpi3_prod_firmware_staging:
context: farmbot-staging
filters:
branches:
only:
- staging
requires:
- build_rpi3_prod_firmware

2
.gitignore vendored
View File

@ -57,4 +57,4 @@ RELEASE_NOTES
.elixir_ls
nerves-hub
*.pem
*.pem

View File

@ -69,7 +69,11 @@ config :farmbot,
global_overlay_dir = "rootfs_overlay"
config :nerves, :firmware, rootfs_overlay: [global_overlay_dir]
config :nerves, :firmware,
rootfs_overlay: [global_overlay_dir],
provisioning: :nerves_hub
import_config("nerves_hub.exs")
case target do
"host" ->
@ -82,5 +86,7 @@ case target do
do: import_config("target/#{target}.exs")
if File.exists?(custom_rootfs_overlay_dir),
do: config :nerves, :firmware, rootfs_overlay: [global_overlay_dir, custom_rootfs_overlay_dir]
do: config :nerves, :firmware,
rootfs_overlay: [global_overlay_dir, custom_rootfs_overlay_dir],
provisioning: :nerves_hub
end

View File

@ -45,8 +45,15 @@ config :farmbot, default_server: "https://staging.farm.bot"
config :farmbot, :behaviour, [
authorization: Farmbot.Bootstrap.Authorization,
system_tasks: Farmbot.Host.SystemTasks,
update_handler: Farmbot.Host.UpdateHandler,
nerves_hub_handler: Farmbot.Host.NervesHubHandler,
# firmware_handler: Farmbot.Firmware.UartHandler
]
config :nerves_runtime,
enable_syslog: false,
target: "host",
kernel: [
autoload_modules: false
]
config :farmbot, :uart_handler, tty: "/dev/ttyACM0"

View File

@ -27,7 +27,8 @@ config :farmbot, :farmware, first_part_farmware_manifest_url: nil
config :farmbot, :behaviour,
authorization: Farmbot.Bootstrap.Authorization,
system_tasks: Farmbot.Test.SystemTasks,
update_handler: FarmbotTestSupport.TestUpdateHandler
update_handler: FarmbotTestSupport.TestUpdateHandler,
nerves_hub_handler: Farmbot.Host.NervesHubHandler
config :farmbot, Farmbot.Repo, [
adapter: Sqlite.Ecto2,

View File

@ -0,0 +1,9 @@
use Mix.Config
config :nerves_hub,
client: Farmbot.System.NervesHubClient,
public_keys: [File.read!("priv/staging.pub"), File.read!("priv/prod.pub")]
config :nerves_hub, NervesHub.Socket, [
reconnect_interval: 5_000,
]

View File

@ -33,6 +33,9 @@ config :farmbot, :init, [
# Allows for first boot configuration.
Farmbot.Target.Bootstrap.Configurator,
# Handles OTA updates from NervesHub
Farmbot.System.NervesHubClient,
# Start up Network
Farmbot.Target.Network,
@ -76,9 +79,9 @@ config :farmbot, :behaviour,
authorization: Farmbot.Bootstrap.Authorization,
system_tasks: Farmbot.Target.SystemTasks,
firmware_handler: Farmbot.Firmware.StubHandler,
update_handler: Farmbot.Target.UpdateHandler,
pin_binding_handler: Farmbot.Target.PinBinding.AleHandler,
leds_handler: Farmbot.Target.Leds.AleHandler
leds_handler: Farmbot.Target.Leds.AleHandler,
nerves_hub_handler: Farmbot.System.NervesHubClient
local_file = Path.join(System.user_home!(), ".ssh/id_rsa.pub")
local_key = if File.exists?(local_file), do: [File.read!(local_file)], else: []
@ -86,6 +89,14 @@ local_key = if File.exists?(local_file), do: [File.read!(local_file)], else: []
config :nerves_network, regulatory_domain: "US"
config :nerves_firmware_ssh, authorized_keys: local_key
config :nerves_init_gadget,
ifname: "usb0",
address_method: :dhcpd,
mdns_domain: "farmbot.local",
node_name: nil,
node_host: :mdns_domain
config :shoehorn,
init: [:nerves_runtime, :nerves_init_gadget, :nerves_firmware_ssh],
handler: Farmbot.ShoehornHandler,
app: :farmbot

View File

@ -17,7 +17,7 @@ config :farmbot, Farmbot.System.ConfigStorage,
database: "/root/config-#{Mix.env()}.sqlite3"
config :farmbot, ecto_repos: [Farmbot.Repo, Farmbot.System.ConfigStorage]
config :logger, LoggerBackendSqlite, [
database: "/root/debug_logs.sqlite3",
max_logs: 10000
@ -33,6 +33,9 @@ config :farmbot, :init, [
# Allows for first boot configuration.
Farmbot.Target.Bootstrap.Configurator,
# Handles OTA updates from NervesHub
Farmbot.System.NervesHubClient,
# Start up Network
Farmbot.Target.Network,
@ -75,9 +78,9 @@ config :farmbot, :behaviour,
authorization: Farmbot.Bootstrap.Authorization,
system_tasks: Farmbot.Target.SystemTasks,
firmware_handler: Farmbot.Firmware.StubHandler,
update_handler: Farmbot.Target.UpdateHandler,
pin_binding_handler: Farmbot.Target.PinBinding.AleHandler,
leds_handler: Farmbot.Target.Leds.AleHandler
leds_handler: Farmbot.Target.Leds.AleHandler,
nerves_hub_handler: Farmbot.System.NervesHubClient
config :nerves_network, regulatory_domain: "US"
config :shoehorn,

161
docs/RELEASE.md 100644
View File

@ -0,0 +1,161 @@
## Provisioning the Release System
Publishing a FarmBotOS release requires coordination of a few different systems.
* FarmBot Web App
* FarmBot OS
* NervesHub
* CircleCI
* GitHub branches and releases
## Legacy System
The legacy system is somewhat simpiler. It goes as follows:
### Pull request into `master` branch.
```
git checkout master
git merge staging
git push origin master
```
Obviously this will not actually work because of testing and things, but that
is what happens behind the scenes on GitHub.
### CircleCI builds release
Once merged into master CircleCI will create a `draft` release on GitHub. This
must be QA'd and confirmed manually before publishing. Once published, FarmBot
will check the `OS_AUTO_UPDATE_URL` in the JWT.
### Beta updates
Users may opt into beta updates by settings `os_beta_updates: true` on their
device's `FbosConfig` endpoint.
The system works the same as production, except that the release is drafted based
on the `beta` branch. The other change is that CircleCI publishes a _real_ release
overwriting a previous release of this version if it exists. the release is tagged
as `pre_release: true` in GitHub releases. this prevents the production system
from downloading `beta` updates.
## NervesHub System
The NervesHub system is simpiler to use, but more complex to setup.
### User registration
Create a admin user. This should be the same `ADMIN_EMAIL` used in
the WebApp configuration.
```
mix nerves_hub.user register
Email address: admin@farmbot.io
Name: farmbot
NervesHub password: *super secret*
Local password: *super duper secret*
```
```
mix nerves_hub.product create
name: farmbot
Local password: *super duper secret*
```
### Signing keys
Now a choice will need to be made.
If fwup signing keys existed beforehand (they did for FarmBot Inc) do:
```
mix nerves_hub.key import <PATH/TO/PUBLIC/KEY> <PATH/TO/PRIVATE/KEY>
Local password: *super duper secret*
```
If new keys are required (probably named "prod") do:
```
mix nerves_hub.key create <NAME>
Local password: *super duper secret*
```
### Exporting certs and keys
The API and CI need copies of these keys and certs.
```
mix nerves_hub.user cert export
Local password: *super duper secret*
User certs exported to: <PATH/TO/EXPORTED_CERTS.tar.gz>
tar -xf <PATH/TO/EXPORTED_CERTS.tar.gz> -C nerves-hub/
```
```
mix nerves_hub.key export prod
Local password: *super duper secret*
Fwup keys exported to: <PATH/TO/EXPORTED_KEYS.tar.gz>
tar -xf <PATH/TO/EXPORTED_KEYS.tar.gz> -C nerves-hub/
```
You will also need the CA cert bundle for the WebApp:
(this may only work for BASH)
```bash
{ curl -s https://raw.githubusercontent.com/nerves-hub/nerves_hub_cli/master/priv/ca_certs/root-ca.pem | head -20 \
&& curl -s https://raw.githubusercontent.com/nerves-hub/nerves_hub_cli/master/priv/ca_certs/intermediate-server-ca.pem | head -20 \
&& curl -s https://raw.githubusercontent.com/nerves-hub/nerves_hub_cli/master/priv/ca_certs/intermediate-user-ca.pem | head -20;
} > nerves-hub/nerves-hub-ca-certs.pem
```
Now the FarmBot API needs the values of in it's environment:
* `NERVES_HUB_KEY` -> `cat nerves-hub/key.pem`
* `NERVES_HUB_CERT` -> `cat nerves-hub/cert.pem`
* `NERVES_HUB_CA` -> `cat nerves-hub/nerves-hub-ca-certs.pem`
CircleCI will need:
* `NERVES_HUB_KEY` -> `cat nerves-hub/key.pem`
* `NERVES_HUB_CERT` -> `cat nerves-hub/cert.pem`
* `NERVES_HUB_FW_PRIVATE_KEY` -> `cat nerves-hub/<KEY NAME>.priv`
* `NERVES_HUB_FW_PUBLIC_KEY` -> `cat nerves-hub/<KEY NAME>.pub`
### Provisioning and Tags
Tags/Deployments follow this structure:
```json
[
"application:<MIX_ENV>",
"channel:<CHANNEL>"
]
```
NOTE: the tags **NOT** json objects, they are simple strings
split by a `:` character. This is done _only_ for readability.
where `MIX_ENV` will be one of:
* `dev`
* `prod`
and `CHANNEL` will be one of:
* `beta`
* `stable`
There should be at least one deployment matching the following
tags:
* `["application:dev", "channel:stable"]`
* a development FBOS release on the `stable` channel
* `["application:prod", "channel:stable"]`
* a production FBOS release on the `stable` channel
* `["application:dev", "channel:beta"]`
* a development FBOS release on the `beta` channel
* `["application:prod", "channel:beta"]`
* a production FBOS release on the `beta` channel
* `["application:dev", "channel:stable"]`
* a development FBOS release on the `stable` channel
* `["application:prod", "channel:stable"]`
* a production FBOS release on the `stable` channel
* `["application:dev", "channel:beta"]`
* a development FBOS release on the `beta` channel
* `["application:prod", "channel:beta"]`
* a production FBOS release on the `beta` channel
### First time setup
```
heroku config:set NERVES_HUB_CERT="$NERVES_HUB_CERT" --app=$HEROKU_APPNAME
heroku config:set NERVES_HUB_KEY="$NERVES_HUB_KEY" --app=$HEROKU_APPNAME
heroku config:set NERVES_HUB_CA="$NERVES_HUB_CA" --app=$HEROKU_APPNAME
heroku config:set NERVES_HUB_ORG="$NERVES_HUB_ORG" --app=$HEROKU_APPNAME
```

View File

@ -3,7 +3,6 @@ defmodule Farmbot do
Supervises the individual modules that make up the Farmbot Application.
This is the entry point of the application.
"""
require Farmbot.Logger
require Logger
use Application
@ -20,6 +19,7 @@ defmodule Farmbot do
def init([]) do
children = [
{Farmbot.System.Registry, []},
{Farmbot.Logger.Supervisor, []},
{Farmbot.System.Supervisor, []},
{Farmbot.Bootstrap.Supervisor, []}

View File

@ -52,6 +52,10 @@ defmodule Farmbot.BotState do
end
end
def set_update_available(bool) when is_boolean(bool) do
GenStage.call(__MODULE__, {:set_update_available, bool})
end
def report_disk_usage(percent) when is_number(percent) do
GenStage.call(__MODULE__, {:report_disk_usage, percent})
end
@ -201,6 +205,12 @@ defmodule Farmbot.BotState do
{:noreply, [], state}
end
def handle_call({:set_update_available, bool}, _from, state) do
new_info_settings = %{state.informational_settings | update_available: bool}
state = %{state | informational_settings: new_info_settings}
{:reply, :ok, [state], state}
end
def handle_call({:report_disk_usage, percent}, _from, state) do
new_info_settings = %{state.informational_settings | disk_usage: percent}
state = %{state | informational_settings: new_info_settings}
@ -391,6 +401,7 @@ defmodule Farmbot.BotState do
defstruct [
informational_settings: %{
update_available: false,
controller_version: @version,
firmware_version: "disconnected",
firmware_commit: @arduino_commit,
@ -405,7 +416,7 @@ defmodule Farmbot.BotState do
locked: false,
cache_bust: 0,
soc_temp: 0, # degrees celcius
wifi_level: nil, # decibels
wifi_level: nil, # decibels
uptime: 0, # seconds
memory_usage: 0, # megabytes
disk_usage: 0, # percent

View File

@ -145,7 +145,9 @@ defmodule Farmbot.BotState.Transport.AMQP do
{:noreply, [], state}
end
def handle_info({:basic_deliver, payload, %{routing_key: key}}, state) do
def handle_info({:basic_deliver, payload, %{routing_key: key} = options}, state) do
# TODO(Connor) - investigate acking
# :ok = Basic.ack(state.chan, options[:delivery_tag])
if GenServer.whereis(Farmbot.Repo) do
device = state.bot
route = String.split(key, ".")
@ -165,6 +167,8 @@ defmodule Farmbot.BotState.Transport.AMQP do
["bot", ^device, "logs"] -> {:noreply, [], state}
["bot", ^device, "status"] -> {:noreply, [], state}
["bot", ^device, "from_device"] -> {:noreply, [], state}
["bot", ^device, "nerves_hub"] ->
handle_nerves_hub(payload, options, state)
_ ->
Logger.warn 3, "got unknown routing key: #{key}"
{:noreply, [], state}
@ -268,6 +272,19 @@ defmodule Farmbot.BotState.Transport.AMQP do
end
end
def handle_nerves_hub(payload, options, state) do
:ok = Basic.ack(state.chan, options[:delivery_tag])
case Poison.decode(payload) do
{:ok, data} ->
cert = data["cert"] |> Base.decode64!()
key = data["key"] |> Base.decode64!()
:ok = Farmbot.System.NervesHub.configure_certs(cert, key)
:ok = Farmbot.System.NervesHub.connect()
{:noreply, [], state}
_ -> {:noreply, [], state}
end
end
defp push_bot_log(chan, bot, log) do
json = Poison.encode!(log)
:ok = AMQP.Basic.publish chan, @exchange, "bot.#{bot}.logs", json
@ -306,6 +323,7 @@ defmodule Farmbot.BotState.Transport.AMQP do
q_base <- device,
:ok <- Basic.qos(chan, [global: true]),
{:ok, _} <- AMQP.Queue.declare(chan, q_base <> "_from_clients", [auto_delete: true]),
from_clients <- [routing_key: "bot.#{device}.from_clients"],
{:ok, _} <- AMQP.Queue.purge(chan, q_base <> "_from_clients"),
@ -315,8 +333,14 @@ defmodule Farmbot.BotState.Transport.AMQP do
sync <- [routing_key: "bot.#{device}.sync.#"],
:ok <- AMQP.Queue.bind(chan, q_base <> "_auto_sync", @exchange, sync),
{:ok, _} <- AMQP.Queue.declare(chan, q_base <> "_nerves_hub", [auto_delete: false, durable: true]),
nerves_hub <- [routing_key: "bot.#{device}.nerves_hub"],
:ok <- AMQP.Queue.bind(chan, q_base <> "_nerves_hub", @exchange, nerves_hub),
{:ok, _tag} <- Basic.consume(chan, q_base <> "_from_clients", self(), [no_ack: true]),
{:ok, _tag} <- Basic.consume(chan, q_base <> "_auto_sync", self(), [no_ack: true]) do
{:ok, _tag} <- Basic.consume(chan, q_base <> "_auto_sync", self(), [no_ack: true]),
{:ok, _tag} <- Basic.consume(chan, q_base <> "_nerves_hub", self(), [])
do
%State{conn: conn, chan: chan, bot: device}
end
end

View File

@ -14,6 +14,8 @@ defmodule Farmbot.CeleryScript.AST.Node.ChangeOwnership do
email = pair_map["email"]
secret = pair_map["secret"] |> Base.decode64!(padding: false, ignore: :whitespace)
server = pair_map["server"] || get_config_value(:string, "authorization", "server")
:ok = Farmbot.System.NervesHub.deconfigure()
case test_credentials(email, secret, server) do
{:ok, _token} ->
Logger.warn 1, "Farmbot is changing ownership to #{email} - #{server}."

View File

@ -5,15 +5,7 @@ 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
{:error, reason} -> {:error, reason, env}
nil -> {:ok, env}
{%Version{} = version, url} ->
case Farmbot.System.Updates.download_and_apply_update({version, url}) do
:ok -> {:ok, env}
{:error, reason} -> {:error, reason, env}
end
end
nerves_hub_updater(env)
end
def execute(%{package: :arduino_firmware}, _, env) do
@ -25,4 +17,11 @@ defmodule Farmbot.CeleryScript.AST.Node.CheckUpdates do
env = mutate_env(env)
Farmbot.CeleryScript.AST.Node.UpdateFarmware.execute(args, [], env)
end
defp nerves_hub_updater(env) do
case Farmbot.System.NervesHub.check_update() do
nil -> {:ok, env}
url when is_binary(url) -> {:ok, env}
end
end
end

View File

@ -4,6 +4,7 @@ defmodule Farmbot.Project do
@version Mix.Project.config[:version]
@target Mix.Project.config[:target]
@commit Mix.Project.config[:commit]
@branch Mix.Project.config[:branch]
@arduino_commit Mix.Project.config[:arduino_commit]
@env Mix.env()
@ -15,6 +16,10 @@ defmodule Farmbot.Project do
@compile {:inline, commit: 0}
def commit, do: @commit
@doc "*#{@branch}*"
@compile {:inline, branch: 0}
def branch, do: @branch
@doc "*#{@arduino_commit}*"
@compile {:inline, arduino_commit: 0}
def arduino_commit, do: @arduino_commit

View File

@ -11,6 +11,7 @@ defmodule Farmbot.System.Init.FSCheckup do
@version Farmbot.Project.version()
@target Farmbot.Project.target()
@env Farmbot.Project.env()
System.put_env("NERVES_FW_VCS_IDENTIFIER", @ref)
@doc false
def start_link(_, opts \\ []) do
@ -68,7 +69,7 @@ defmodule Farmbot.System.Init.FSCheckup do
try do
Elixir.Logger.add_backend(LoggerBackendSqlite)
catch
:exit, r ->
:exit, r ->
Logger.error 1, "Could not start disk logging: #{inspect r}"
Elixir.Logger.remove_backend(LoggerBackendSqlite)
File.rm(Path.join([@data_path, "root", "debug_logs.sqlite3"]))

View File

@ -0,0 +1,29 @@
defmodule Farmbot.System.Init.Suprevisor do
@moduledoc false
use Supervisor
import Farmbot.System.Init
def start_link(args \\ []) do
Supervisor.start_link(__MODULE__, args, [name: __MODULE__])
end
def init([]) do
init_mods =
Application.get_env(:farmbot, :init)
|> Enum.map(fn child -> fb_init(child, [[], [name: child]]) end)
children = [
# Load kernel modules
worker(Farmbot.System.Init.KernelMods, [[], []]),
# Ensure filesystem
worker(Farmbot.System.Init.FSCheckup, [[], []]),
# Ensure ecto + migrations
supervisor(Farmbot.System.Init.Ecto, [[], []]),
# Ensure config_storage
supervisor(Farmbot.System.ConfigStorage, []),
worker(Farmbot.System.ConfigStorage.Dispatcher, []),
] ++ init_mods
Supervisor.init(children, [strategy: :one_for_one])
end
end

View File

@ -0,0 +1,142 @@
defmodule Farmbot.System.NervesHub do
@moduledoc """
Wrapper for NervesHub that can support both host and target environments.
Some things can be configured via Mix.Config:
config :farmbot, Farmbot.System.NervesHub, [
app_env: "application:some_other_tag",
extra_tags: ["some", "more", "tags"]
]
## On Target Devices
FarmBotOS requires some weird behaviour. `:nerves_hub` should not be started
with the rest of the application. Connecting should input serial, cert and key
into NervesRuntimeKV, restart NervesRuntimeKV, then _finally_ start `:nerves_hub`.
## On host.
Just return :ok to everything.
"""
@handler Application.get_env(:farmbot, :behaviour)[:nerves_hub_handler]
|| Mix.raise("missing :nerves_hub_handler module")
@doc "Function to return a String serial number. "
@callback serial_number() :: String.t()
@doc "Connect to NervesHub."
@callback connect() :: :ok | :error
@doc "Burn the serial number into persistent storage."
@callback provision(serial :: String.t) :: :ok | :error
@doc "Burn the cert and key into persistent storage."
@callback configure_certs(cert :: String.t(), key :: String.t()) :: :ok | :error
@doc "Return the current confuration including serial, cert and key."
@callback config() :: [String.t() | nil]
@doc "Remove serial, cert, and key from persistent storage."
@callback deconfigure() :: :ok | :error
@doc "Should return a url to an update or nil."
@callback check_update() :: String.t() | nil
use GenServer
use Farmbot.Logger
def start_link(args \\ []) do
GenServer.start_link(__MODULE__, args, [name: __MODULE__])
end
def init([]) do
send self(), :configure
{:ok, :not_configured}
end
def terminate(reason, state) do
Logger.warn 1, "Nerves Hub crash: #{inspect reason} when in state: #{inspect state}"
end
def handle_info(:configure, :not_configured) do
channel = case Farmbot.Project.branch() do
"master" -> "channel:stable"
"beta" -> "channel:beta"
"staging" -> "channel:staging"
branch -> "channel:#{branch}"
end
if Process.whereis(Farmbot.HTTP) do
app_config = Application.get_env(:farmbot, __MODULE__, [])
"application:" <> _ = app_env = app_config[:app_env] || "application:#{Farmbot.Project.env()}"
extra_tags = app_config[:extra_tags] || []
if nil in get_config() do
Logger.info 1, "doing initial nerves hub configuration."
:ok = deconfigure()
:ok = provision()
:ok = configure([app_env, channel] ++ extra_tags)
else
connect()
end
{:noreply, :configured}
else
Logger.debug 3, "Server not configured yet. Waiting 10_000 ms to try again."
Process.send_after(self(), :configure, 10_000)
{:noreply, :not_configured}
end
end
def get_config do
@handler.config()
end
def connect do
Logger.debug 1, "Connecting to NervesHub"
@handler.connect()
end
# Returns the current serial number.
def serial do
@handler.serial_number()
end
# Sets Serial number in environment.
def provision do
Logger.debug 1, "Provisioning NervesHub"
:ok = @handler.provision(serial())
end
# Creates a device in NervesHub
# or updates it if one exists.
def configure(tags) when is_list(tags) do
Logger.debug 1, "Configuring NervesHub: #{inspect tags}"
payload = %{
serial_number: serial(),
tags: tags
} |> Farmbot.JSON.encode!()
_ = Farmbot.HTTP.post!("/api/device_cert", payload)
:ok
end
# Message comes over AMQP.
def configure_certs("-----BEGIN CERTIFICATE-----" <> _ = cert,
"-----BEGIN EC PRIVATE KEY-----" <> _ = key) do
Logger.debug 1, "Configuring certs for NervesHub."
:ok = @handler.configure_certs(cert, key)
:ok
end
def deconfigure do
Logger.debug 1, "Deconfiguring NervesHub"
:ok = @handler.deconfigure()
:ok
end
def check_update do
Logger.debug 1, "Check update NervesHub"
@handler.check_update()
end
end

View File

@ -1,10 +1,11 @@
defmodule Farmbot.System.Registry do
@moduledoc "Farmbot System Global Registry"
@reg FarmbotRegistry
use GenServer
@doc false
def start_link do
GenServer.start_link(__MODULE__, [], [name: __MODULE__])
def start_link(args) do
GenServer.start_link(__MODULE__, args, [name: __MODULE__])
end
@doc "Dispatch a global event from a namespace."

View File

@ -3,7 +3,6 @@ defmodule Farmbot.System.Supervisor do
Supervises Platform specific stuff for Farmbot to operate
"""
use Supervisor
import Farmbot.System.Init
@doc false
def start_link(args) do
@ -11,25 +10,11 @@ defmodule Farmbot.System.Supervisor do
end
def init([]) do
before_init_children = [
worker(Farmbot.System.Registry, []),
worker(Farmbot.System.Init.KernelMods, [[], []]),
worker(Farmbot.System.Init.FSCheckup, [[], []]),
supervisor(Farmbot.System.Init.Ecto, [[], []]),
supervisor(Farmbot.System.ConfigStorage, []),
worker(Farmbot.System.ConfigStorage.Dispatcher, []),
]
init_mods =
Application.get_env(:farmbot, :init)
|> Enum.map(fn child -> fb_init(child, [[], [name: child]]) end)
after_init_children = [
supervisor(Farmbot.System.Updates, []),
children = [
supervisor(Farmbot.System.Init.Suprevisor, []),
worker(Farmbot.System.NervesHub, []),
worker(Farmbot.EasterEggs, []),
]
all_children = before_init_children ++ init_mods ++ after_init_children
Supervisor.init(all_children, strategy: :one_for_all)
Supervisor.init(children, strategy: :one_for_one)
end
end

View File

@ -68,7 +68,7 @@ defmodule Farmbot.System do
_, _ ->
Logger.error 1, "Firmware unavailable. Can't emergency_lock"
end
end
defp do_reset(reason) do
@ -77,6 +77,7 @@ defmodule Farmbot.System do
nil -> reboot("Escape factory reset: #{inspect reason}")
{:ignore, reason} -> reboot(reason)
_ ->
Farmbot.System.NervesHub.deconfigure()
path = Farmbot.Farmware.Installer.install_root_path()
File.rm_rf(path)
Ecto.drop()

View File

@ -1,18 +0,0 @@
defmodule Farmbot.System.UpdateHandler do
@moduledoc "Behaviour for setting up OTA updates."
@doc "Called before and update."
@callback before_update :: :ok | {:error, term}
@doc "Called after a reboot."
@callback post_update :: :ok | {:error, term}
@doc "Apply a fw update."
@callback apply_firmware(Path.t) :: :ok | {:error, term}
@doc "Setup updates."
@callback setup(atom) :: :ok | {:error, term}
@doc "If a fw has already been applied."
@callback requires_reboot? :: boolean
end

View File

@ -1,59 +0,0 @@
defmodule Farmbot.System.UpdateTimer do
@moduledoc false
@twelve_hours 4.32e+7 |> round()
use GenServer
use Farmbot.Logger
def wait_for_http(callback) do
case Process.whereis(Farmbot.HTTP) do
nil ->
Process.sleep(1000)
wait_for_http(callback)
pid when is_pid(pid) ->
do_check()
Process.send_after(callback, :checkup, @twelve_hours)
end
end
def start_link 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()]
Farmbot.System.Registry.subscribe(self())
{:ok, []}
end
def handle_info(:checkup, state) do
do_check()
Process.send_after(self(), :checkup, @twelve_hours)
{:noreply, state}
end
def handle_info({Farmbot.System.Registry, {:config_storage, {"settings", "beta_opt_in", true}}}, state) do
if Process.whereis(Farmbot.Bootstrap.AuthTask) do
Logger.debug 3, "Opted into beta updates. Refreshing token."
Farmbot.Bootstrap.AuthTask.force_refresh()
end
{:noreply, state}
end
def handle_info({Farmbot.System.Registry, _info}, state) do
{:noreply, state}
end
defp do_check do
osau = Farmbot.System.ConfigStorage.get_config_value(:bool, "settings", "os_auto_update")
case Farmbot.System.Updates.check_updates() do
{:error, err} -> Logger.error 1, "Error checking for updates: #{inspect err}"
nil -> Logger.debug 3, "No updates available as of #{inspect Timex.now()}"
url -> if osau, do: Farmbot.System.Updates.download_and_apply_update(url)
end
end
end

View File

@ -1,326 +0,0 @@
defmodule Farmbot.System.Updates do
@moduledoc "Handles over the air updates."
use Supervisor
use Farmbot.Logger
alias Farmbot.System.ConfigStorage
import ConfigStorage, only: [get_config_value: 3, update_config_value: 4]
@data_path Application.get_env(:farmbot, :data_path)
@target Farmbot.Project.target()
@current_version Farmbot.Project.version()
@env Farmbot.Project.env()
@update_handler Application.get_env(:farmbot, :behaviour)[:update_handler]
@update_handler || Mix.raise("Please configure update_handler")
@doc "Overwrite os update server field"
def override_update_server(url) do
update_config_value(:string, "settings", "os_update_server_overwrite", url)
end
defmodule Release do
@moduledoc false
defmodule Asset do
@moduledoc false
defstruct [:name, :browser_download_url]
end
defstruct [
tag_name: nil,
target_commitish: nil,
name: nil,
draft: false,
prerelease: true,
body: nil,
assets: []
]
end
defmodule CurrentStuff do
@moduledoc false
import Farmbot.Project
defstruct [
:token,
:beta_opt_in,
:os_update_server_overwrite,
:currently_on_beta,
:env,
:commit,
:target,
:version
]
@doc "Get the current stuff. Fields can be replaced for testing."
def get(replace \\ %{}) do
os_update_server_overwrite = get_config_value(:string, "settings", "os_update_server_overwrite")
beta_opt_in? = is_binary(os_update_server_overwrite) || get_config_value(:bool, "settings", "beta_opt_in")
token_bin = get_config_value(:string, "authorization", "token")
currently_on_beta? = get_config_value(:bool, "settings", "currently_on_beta")
token = if token_bin, do: Farmbot.Jwt.decode!(token_bin), else: nil
opts = %{
token: token,
beta_opt_in: beta_opt_in?,
currently_on_beta: currently_on_beta?,
os_update_server_overwrite: os_update_server_overwrite,
env: env(),
commit: commit(),
target: target(),
version: version()
} |> Map.merge(Map.new(replace))
struct(__MODULE__, opts)
end
end
@doc "Downloads and applies an update file."
def download_and_apply_update({%Version{} = version, dl_url}) do
if @update_handler.requires_reboot?() do
Logger.warn 1, "Can't apply update. An update is already staged. Please reboot and try again."
{:error, :reboot_required}
else
fe_constant = "FBOS_OTA"
dl_fun = Farmbot.BotState.download_progress_fun(fe_constant)
# TODO(Connor): I'd like this to have a version number..
dl_path = Path.join(@data_path, "ota.fw")
results = http_adapter().download_file(dl_url, dl_path, dl_fun, "", [])
Farmbot.BotState.clear_progress_fun(fe_constant)
case results do
{:ok, path} -> apply_firmware("beta" in (version.pre || []), path, true)
{:error, reason} -> {:error, reason}
end
end
end
@doc """
Force check for updates.
Does _NOT_ download or apply update.
"""
def check_updates(release \\ nil, current_stuff \\ nil)
# All the HTTP Requests happen here.
def check_updates(nil, current_stuff) do
# Get current values.
current_stuff_mut = %{
token: token,
beta_opt_in: beta_opt_in,
os_update_server_overwrite: server_override,
env: env,
} = current_stuff || CurrentStuff.get()
cond do
# Don't allow non producion envs to check production env updates.
env != :prod -> {:error, :wrong_env}
# Don't check if the token is nil.
is_nil(token) -> {:error, :no_token}
# Allows the server to be overwrote.
is_binary(server_override) ->
Logger.debug 3, "Update server override: #{server_override}"
get_release_from_url(server_override)
# Beta updates should check twice.
beta_opt_in ->
Logger.debug 3, "Checking for beta updates."
token
|> Map.get(:beta_os_update_server)
|> get_release_from_url()
# Conditions exhausted. We _must_ be on a production release.
true ->
Logger.debug 3, "Checking for production updates."
token
|> Map.get(:os_update_server)
|> get_release_from_url()
end
|> case do
# Beta needs to make two requests:
# check for a later beta update, if no later beta update,
# Check for a later production release.
%Release{} = release when beta_opt_in ->
do_check_production_release = fn() ->
token
|> Map.get(:os_update_server)
|> get_release_from_url()
|> case do
%Release{} = prod_release -> check_updates(prod_release, current_stuff_mut)
err -> err
end
end
check_updates(release, current_stuff_mut) || do_check_production_release.()
# Production release; no beta. Check the release for an asset.
%Release{} = release -> check_updates(release, current_stuff_mut)
err -> err
end
end
# Check against the release struct. Not HTTP requests from here out.
def check_updates(%Release{} = rel, %CurrentStuff{} = current_stuff) do
%{
beta_opt_in: beta_opt_in,
currently_on_beta: currently_on_beta,
commit: current_commit,
version: current_version,
} = current_stuff
release_version = String.trim(rel.tag_name, "v") |> Version.parse!()
is_beta_release? = "beta" in (release_version.pre || [])
version_comp = Version.compare(current_version, release_version)
release_commit = rel.target_commitish
commits_equal? = current_commit == release_commit
prerelease = rel.prerelease
cond do
# Don't bother if the release is a draft. Not sure how/if this can happen.
rel.draft ->
Logger.warn 1, "Not checking draft release."
nil
# Only check prerelease if
# current_version is less than or equal to release_version
# AND
# the commits are not equal.
prerelease and is_beta_release? and beta_opt_in and !commits_equal? ->
# beta release get marked as greater than non beta release, so we need
# to manually check the versions by removing the pre part.
case Version.compare(current_version, %{release_version | pre: []}) do
:lt ->
Logger.debug 3, "Current version (#{current_version}) is less than beta release (#{release_version})"
try_find_dl_url_in_asset(rel.assets, release_version, current_stuff)
:eq ->
if currently_on_beta do
Logger.debug 3, "Current version (#{current_version}) (beta updates enabled) is equal to beta release (#{release_version})"
try_find_dl_url_in_asset(rel.assets, release_version, current_stuff)
else
Logger.debug 3, "Current version (#{current_version}) (beta updates disabled) is equal to latest beta (#{release_version})"
nil
end
:gt ->
Logger.debug 3, "Current version (#{current_version}) is greater than latest beta release (#{release_version})"
nil
end
# if the current version is less than the release version.
!prerelease and version_comp == :lt ->
Logger.debug 3, "Current version is less than release."
try_find_dl_url_in_asset(rel.assets, release_version, current_stuff)
# If the version isn't different, but the commits are different,
# This happens for beta releases.
!prerelease and version_comp == :eq and !commits_equal? ->
Logger.debug 3, "Current version is equal to release, but commits are not equal."
try_find_dl_url_in_asset(rel.assets, release_version, current_stuff)
# Conditions exhausted. No updates available.
true ->
comparison_str = "version check: current version: #{current_version} #{version_comp} latest release version: #{release_version} \n"<>
"commit check: current commit: #{current_commit} latest release commit: #{release_commit}: (equal: #{commits_equal?})"
Logger.debug 3, "No updates available: \ntarget: #{Farmbot.Project.target()}: \nprerelease: #{prerelease} \n#{comparison_str}"
nil
end
end
@doc "Finds a asset url if it exists, nil if not."
@spec try_find_dl_url_in_asset([%Release.Asset{}], Version.t, %CurrentStuff{}) :: {Version.t, String.t}
def try_find_dl_url_in_asset(assets, version, current_stuff)
def try_find_dl_url_in_asset([%Release.Asset{name: name, browser_download_url: bdurl} | rest], %Version{} = release_version_obj, current_stuff) do
release_version = to_string(release_version_obj)
current_target = to_string(current_stuff.target)
expected_name = "farmbot-#{current_target}-#{release_version}.fw"
if match?(^expected_name, name) do
{release_version_obj, bdurl}
else
Logger.debug 3, "Incorrect asset name for target: #{current_target}: #{name}"
try_find_dl_url_in_asset(rest, release_version_obj, current_stuff)
end
end
def try_find_dl_url_in_asset([], release_version, current_stuff) do
Logger.warn 2, "No update in assets for #{current_stuff.target()} for #{release_version}"
nil
end
@doc "HTTP request to fetch a Release."
def get_release_from_url(url) when is_binary(url) do
Logger.debug 3, "Checking for updates: #{url}"
case http_adapter().get(url) do
# This can happen on beta updates, if on an old token
# and a new beta release was published.
{:ok, %{status_code: 404}} ->
Logger.warn 1, "Got a 404 checking for updates: #{url}. Fetching a new token. Try that again"
Farmbot.Bootstrap.AuthTask.force_refresh()
{:error, :token_refresh}
# Decode the HTTP body as a release.
{:ok, %{status_code: 200, body: body}} ->
pattern = struct(Release, [assets: [struct(Release.Asset)]])
case Poison.decode(body, as: pattern) do
{:ok, %Release{} = rel} -> rel
_err -> {:error, :bad_release_body}
end
# Error situations
{:ok, %{status_code: _code, body: body}} -> {:error, body}
err -> err
end
end
@doc "Apply an OS (fwup) firmware."
def apply_firmware(is_beta?, file_path, reboot) when is_boolean(is_beta?) do
Logger.busy 1, "Applying #{@target} OS update (beta=#{is_beta?})"
before_update()
case @update_handler.apply_firmware(file_path) do
:ok ->
update_config_value(:bool, "settings", "currently_on_beta", is_beta?)
Logger.success 1, "OS Firmware updated!"
if reboot do
Logger.warn 1, "Farmbot going down for OS update."
Farmbot.System.reboot("OS Firmware Update.")
end
{:error, reason} ->
Logger.error 1, "Failed to apply update: #{inspect reason}"
{:error, reason}
end
end
# Private
defp maybe_post_update do
case File.read(update_file()) do
{:ok, @current_version} -> :ok
{:ok, old_version} ->
Logger.info 1, "Updating from #{old_version} to #{@current_version}"
@update_handler.post_update()
{:error, :enoent} ->
Logger.info 1, "Updating to #{@current_version}"
{:error, err} -> raise err
end
before_update()
end
defp before_update, do: File.write!(update_file(), @current_version)
defp update_file, do: Path.join(@data_path, "update")
defp http_adapter, do: Farmbot.HTTP
@doc false
def start_link do
Supervisor.start_link(__MODULE__, [], [name: __MODULE__])
end
@doc false
def init([]) do
case @update_handler.setup(@env) do
:ok ->
maybe_post_update()
children = [
worker(Farmbot.System.UpdateTimer, [])
]
opts = [strategy: :one_for_one]
supervise(children, opts)
{:error, reason} -> {:stop, reason}
end
end
end

26
mix.exs
View File

@ -2,10 +2,13 @@ defmodule Farmbot.Mixfile do
use Mix.Project
@target System.get_env("MIX_TARGET") || "host"
@version Path.join(__DIR__, "VERSION") |> File.read!() |> String.trim()
@commit System.cmd("git", ~w"rev-parse --verify HEAD") |> elem(0) |> String.trim()
@branch System.cmd("git", ~w"rev-parse --abbrev-ref HEAD") |> elem(0) |> String.trim()
System.put_env("NERVES_FW_VCS_IDENTIFIER", @commit)
defp commit do
System.cmd("git", ~w"rev-parse --verify HEAD") |> elem(0) |> String.trim()
end
defp commit, do: @commit
defp branch, do: @branch
defp arduino_commit do
opts = [cd: "c_src/farmbot-arduino-firmware"]
@ -28,6 +31,7 @@ defmodule Farmbot.Mixfile do
version: @version,
target: @target,
commit: commit(),
branch: branch(),
arduino_commit: arduino_commit(),
archives: [nerves_bootstrap: "~> 1.2"],
build_embedded: Mix.env() == :prod,
@ -58,13 +62,23 @@ defmodule Farmbot.Mixfile do
]
end
def application do
def application("host") do
[
mod: {Farmbot, []},
extra_applications: [:logger, :eex, :ssl, :inets, :runtime_tools]
]
end
def application(_target) do
[
mod: {Farmbot, []},
extra_applications: [:logger, :eex, :ssl, :inets, :runtime_tools],
included_applications: [:nerves_hub]
]
end
def application(), do: application(@target)
defp docs do
[
main: "building",
@ -122,7 +136,8 @@ defmodule Farmbot.Mixfile do
{:ring_logger, "~> 0.5"},
{:bbmustache, "~> 1.6"},
{:sqlite_ecto2, "~> 2.2"},
{:logger_backend_sqlite, "~> 2.1"}
{:logger_backend_sqlite, "~> 2.1"},
{:nerves_hub_cli, "~> 0.4"}
]
end
@ -141,6 +156,7 @@ defmodule Farmbot.Mixfile do
system(target) ++
[
{:nerves_runtime, "~> 0.8"},
{:nerves_hub, github: "nerves-hub/nerves_hub", override: true},
{:nerves_firmware, "~> 0.4"},
{:nerves_firmware_ssh, "~> 0.3"},
{:nerves_init_gadget, "~> 0.5", only: :dev},

View File

@ -32,8 +32,7 @@
"hackney": {:hex, :hackney, "1.14.3", "b5f6f5dcc4f1fba340762738759209e21914516df6be440d85772542d4a5e412", [:rebar3], [{:certifi, "2.4.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
"httpoison": {:hex, :httpoison, "1.3.1", "7ac607311f5f706b44e8b3fab736d0737f2f62a31910ccd9afe7227b43edb7f0", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
"inch_ex": {:hex, :inch_ex, "1.0.1", "1f0af1a83cec8e56f6fc91738a09c838e858db3d78ef5f2ec040fe4d5a62dabf", [:mix], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
"jason": {:hex, :jason, "1.1.1", "d3ccb840dfb06f2f90a6d335b536dd074db748b3e7f5b11ab61d239506585eb2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
"joken": {:hex, :joken, "1.5.0", "42a0953e80bd933fc98a0874e156771f78bf0e92abe6c3a9c22feb6da28efb0b", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
"jose": {:hex, :jose, "1.8.4", "7946d1e5c03a76ac9ef42a6e6a20001d35987afd68c2107bcd8f01a84e75aa73", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"},
"jsx": {:hex, :jsx, "2.9.0", "d2f6e5f069c00266cad52fb15d87c428579ea4d7d73a33669e12679e203329dd", [:mix, :rebar3], [], "hexpm"},
@ -48,12 +47,15 @@
"mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"},
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"},
"mock": {:hex, :mock, "0.3.2", "e98e998fd76c191c7e1a9557c8617912c53df3d4a6132f561eb762b699ef59fa", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"},
"nerves": {:hex, :nerves, "1.3.1", "4e2002315b38d38c6bfb5d3e33d7f4a32057037c3f2026af64d0867bc1741b21", [:mix], [{:distillery, "~> 2.0", [hex: :distillery, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
"nerves": {:hex, :nerves, "1.3.4", "9523cc1936f173c99cf15a132c2b24f9c6f1a5cfe3327bbcd518ff7e441327d3", [:mix], [{:distillery, "2.0.10", [hex: :distillery, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
"nerves_hub_cli": {:hex, :nerves_hub_cli, "0.4.0", "d5efcc49179fff8f3cd4542831820082d6abf2290fb5251c85e54ea51614b35d", [:mix], [{:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:pbcs, "~> 0.1", [hex: :pbcs, repo: "hexpm", optional: false]}, {:tesla, "~> 1.2.1 or ~> 1.3", [hex: :tesla, repo: "hexpm", optional: false]}, {:x509, "~> 0.3", [hex: :x509, repo: "hexpm", optional: false]}], "hexpm"},
"nerves_leds": {:hex, :nerves_leds, "0.8.0", "193692767dca1a201b09113d242648493b9be0087bab83ebee99c3b0a254f5e1", [:mix], [], "hexpm"},
"nerves_runtime": {:git, "https://github.com/nerves-project/nerves_runtime.git", "ea804f9326c649681e3fcf72e8dd81d26a6508b6", []},
"nerves_uart": {:hex, :nerves_uart, "1.2.0", "195424116b925cd3bf9d666be036c2a80655e6ca0f8d447e277667a60005c50e", [:mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"},
"net_logger": {:hex, :net_logger, "0.1.0", "59be302c09cf70dab164810c923ccb9a976eda7270e5a32b93ba8aeb850de1d6", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:uuid, "~> 1.1", [hex: :uuid, repo: "hexpm", optional: false]}], "hexpm"},
"nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"},
"pbcs": {:hex, :pbcs, "0.1.0", "6f79ce81d93edf5ac41fcd8b32fb203ad6895ebdb33d115e14a5bd955b90020a", [:mix], [], "hexpm"},
"phoenix_html": {:hex, :phoenix_html, "2.12.0", "1fb3c2e48b4b66d75564d8d63df6d53655469216d6b553e7e14ced2b46f97622", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"plug": {:hex, :plug, "1.6.4", "35618dd2cc009b69b000f785452f6b370f76d099ece199733fea27bc473f809d", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"},
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
@ -70,8 +72,11 @@
"sqlite_ecto2": {:hex, :sqlite_ecto2, "2.2.5", "f111a48188b0640effb7f2952071c4cf285501d3ce090820a7c2fc20af3867e9", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "2.2.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: false]}, {:sqlitex, "~> 1.4", [hex: :sqlitex, repo: "hexpm", optional: false]}], "hexpm"},
"sqlitex": {:hex, :sqlitex, "1.4.3", "a50f12d6aeb25f4ebb128453386c09bbba8f5abd3c7713dc5eaa92f359926ac5", [:mix], [{:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:esqlite, "~> 0.2.4", [hex: :esqlite, repo: "hexpm", optional: false]}], "hexpm"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"},
"tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
"timex": {:hex, :timex, "3.4.1", "e63fc1a37453035e534c3febfe9b6b9e18583ec7b37fd9c390efdef97397d70b", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
"tzdata": {:hex, :tzdata, "0.5.19", "7962a3997bf06303b7d1772988ede22260f3dae1bf897408ebdac2b4435f4e6a", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"},
"uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm"},
"websocket_client": {:hex, :websocket_client, "1.3.0", "2275d7daaa1cdacebf2068891c9844b15f4fdc3de3ec2602420c2fb486db59b6", [:rebar3], [], "hexpm"},
"x509": {:hex, :x509, "0.3.0", "c6f3db66960c6e4f424d1e6cca5c7d730e0a577af8dc115a613f4560ce1df6d3", [:mix], [], "hexpm"},
}

View File

@ -23,6 +23,7 @@
"esqlite": {:hex, :esqlite, "0.2.4", "3a8a352c190afe2d6b828b252a6fbff65b5cc1124647f38b15bdab3bf6fd4b3e", [:rebar3], [], "hexpm"},
"farmbot_system_rpi0": {:hex, :farmbot_system_rpi0, "1.5.1-farmbot.0", "61458e70b5e48dfe9774a0389e4ec4c7935a2f4e4488e94a708dc66f3b904e8c", [:mix], [{:nerves, "~> 1.3", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_system_br, "1.5.3", [hex: :nerves_system_br, repo: "hexpm", optional: false]}, {:nerves_system_linter, "~> 0.3.0", [hex: :nerves_system_linter, repo: "hexpm", optional: false]}, {:nerves_toolchain_armv6_rpi_linux_gnueabi, "1.1.0", [hex: :nerves_toolchain_armv6_rpi_linux_gnueabi, repo: "hexpm", optional: false]}], "hexpm"},
"fs": {:hex, :fs, "3.4.0", "6d18575c250b415b3cad559e6f97a4c822516c7bc2c10bfbb2493a8f230f5132", [:rebar3], [], "hexpm"},
"fwup": {:hex, :fwup, "0.3.0", "2c360815565fcbc945ebbb34b58f156efacb7f8d64766f1cb3426919bb3f41ea", [:mix], [], "hexpm"},
"gen_stage": {:hex, :gen_stage, "0.14.0", "65ae78509f85b59d360690ce3378d5096c3130a0694bab95b0c4ae66f3008fad", [:mix], [], "hexpm"},
"gettext": {:hex, :gettext, "0.16.0", "4a7e90408cef5f1bf57c5a39e2db8c372a906031cc9b1466e963101cb927dafc", [:mix], [], "hexpm"},
"goldrush": {:hex, :goldrush, "0.1.9", "f06e5d5f1277da5c413e84d5a2924174182fb108dabb39d5ec548b27424cd106", [:rebar3], [], "hexpm"},
@ -45,6 +46,8 @@
"nerves": {:hex, :nerves, "1.3.1", "4e2002315b38d38c6bfb5d3e33d7f4a32057037c3f2026af64d0867bc1741b21", [:mix], [{:distillery, "~> 2.0", [hex: :distillery, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
"nerves_firmware": {:hex, :nerves_firmware, "0.4.0", "ac2fed915a7ca4bb69f567d9b742d77cffc3a6a56420ce65e870c8c34119b935", [:mix], [], "hexpm"},
"nerves_firmware_ssh": {:hex, :nerves_firmware_ssh, "0.3.3", "79c42303ddbfd89ae6f5b4b19a4397a6188df21ca0e7a6573c2399e081fb7d25", [:mix], [{:nerves_runtime, "~> 0.4", [hex: :nerves_runtime, repo: "hexpm", optional: false]}], "hexpm"},
"nerves_hub": {:git, "https://github.com/nerves-hub/nerves_hub.git", "2da9a6fea2b53fa9e0251045b06e265e21847d24", []},
"nerves_hub_cli": {:hex, :nerves_hub_cli, "0.4.0", "d5efcc49179fff8f3cd4542831820082d6abf2290fb5251c85e54ea51614b35d", [:mix], [{:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:pbcs, "~> 0.1", [hex: :pbcs, repo: "hexpm", optional: false]}, {:tesla, "~> 1.2.1 or ~> 1.3", [hex: :tesla, repo: "hexpm", optional: false]}, {:x509, "~> 0.3", [hex: :x509, repo: "hexpm", optional: false]}], "hexpm"},
"nerves_init_gadget": {:hex, :nerves_init_gadget, "0.5.1", "07f3eeb9acb3f919b3b34b36f552bb38d70f2d29ace63f3f23f33eee6a1ca693", [:mix], [{:mdns, "~> 1.0", [hex: :mdns, repo: "hexpm", optional: false]}, {:nerves_firmware_ssh, "~> 0.2", [hex: :nerves_firmware_ssh, repo: "hexpm", optional: false]}, {:nerves_network, "~> 0.3", [hex: :nerves_network, repo: "hexpm", optional: false]}, {:nerves_runtime, "~> 0.3", [hex: :nerves_runtime, repo: "hexpm", optional: false]}, {:one_dhcpd, "~> 0.1", [hex: :one_dhcpd, repo: "hexpm", optional: false]}, {:ring_logger, "~> 0.4", [hex: :ring_logger, repo: "hexpm", optional: false]}], "hexpm"},
"nerves_leds": {:hex, :nerves_leds, "0.8.0", "193692767dca1a201b09113d242648493b9be0087bab83ebee99c3b0a254f5e1", [:mix], [], "hexpm"},
"nerves_network": {:hex, :nerves_network, "0.3.7", "200767191b1ded5a61cddbacd3efdce92442cc055bdc37c20ca8c7cb1d964098", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nerves_network_interface, "~> 0.4.4", [hex: :nerves_network_interface, repo: "hexpm", optional: false]}, {:nerves_wpa_supplicant, "~> 0.3", [hex: :nerves_wpa_supplicant, repo: "hexpm", optional: false]}, {:system_registry, "~> 0.7", [hex: :system_registry, repo: "hexpm", optional: false]}], "hexpm"},
@ -62,6 +65,8 @@
"net_logger": {:hex, :net_logger, "0.1.0", "59be302c09cf70dab164810c923ccb9a976eda7270e5a32b93ba8aeb850de1d6", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:uuid, "~> 1.1", [hex: :uuid, repo: "hexpm", optional: false]}], "hexpm"},
"one_dhcpd": {:hex, :one_dhcpd, "0.2.0", "18eb8ce7101ad7b79e67f3d7ee7f648f42e02b8fa4c1cb3f24f403bf6860f81d", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"},
"pbcs": {:hex, :pbcs, "0.1.0", "6f79ce81d93edf5ac41fcd8b32fb203ad6895ebdb33d115e14a5bd955b90020a", [:mix], [], "hexpm"},
"phoenix_channel_client": {:hex, :phoenix_channel_client, "0.3.2", "188f6e4cad20da03e04f685416a86f682c413efca7c72303f479b94662cb897c", [:mix], [{:websocket_client, "~> 1.3", [hex: :websocket_client, repo: "hexpm", optional: true]}], "hexpm"},
"phoenix_html": {:hex, :phoenix_html, "2.12.0", "1fb3c2e48b4b66d75564d8d63df6d53655469216d6b553e7e14ced2b46f97622", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"plug": {:hex, :plug, "1.6.4", "35618dd2cc009b69b000f785452f6b370f76d099ece199733fea27bc473f809d", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"},
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
@ -79,8 +84,11 @@
"sqlitex": {:hex, :sqlitex, "1.4.3", "a50f12d6aeb25f4ebb128453386c09bbba8f5abd3c7713dc5eaa92f359926ac5", [:mix], [{:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:esqlite, "~> 0.2.4", [hex: :esqlite, repo: "hexpm", optional: false]}], "hexpm"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"},
"system_registry": {:hex, :system_registry, "0.8.0", "09240347628b001433d18279a2759ef7237ba7361239890d8c599cca9a2fbbc2", [:mix], [], "hexpm"},
"tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
"timex": {:hex, :timex, "3.4.1", "e63fc1a37453035e534c3febfe9b6b9e18583ec7b37fd9c390efdef97397d70b", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
"tzdata": {:hex, :tzdata, "0.5.19", "7962a3997bf06303b7d1772988ede22260f3dae1bf897408ebdac2b4435f4e6a", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"},
"uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm"},
"websocket_client": {:hex, :websocket_client, "1.3.0", "2275d7daaa1cdacebf2068891c9844b15f4fdc3de3ec2602420c2fb486db59b6", [:rebar3], [], "hexpm"},
"x509": {:hex, :x509, "0.3.0", "c6f3db66960c6e4f424d1e6cca5c7d730e0a577af8dc115a613f4560ce1df6d3", [:mix], [], "hexpm"},
}

View File

@ -23,13 +23,14 @@
"esqlite": {:hex, :esqlite, "0.2.4", "3a8a352c190afe2d6b828b252a6fbff65b5cc1124647f38b15bdab3bf6fd4b3e", [:rebar3], [], "hexpm"},
"farmbot_system_rpi3": {:hex, :farmbot_system_rpi3, "1.5.1-farmbot.0", "6bc583c00beb764fa917a9071a709dfc0e2c296376fcad8c35eb281f5186657f", [:mix], [{:nerves, "~> 1.3", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_system_br, "1.5.3", [hex: :nerves_system_br, repo: "hexpm", optional: false]}, {:nerves_system_linter, "~> 0.3.0", [hex: :nerves_system_linter, repo: "hexpm", optional: false]}, {:nerves_toolchain_arm_unknown_linux_gnueabihf, "1.1.0", [hex: :nerves_toolchain_arm_unknown_linux_gnueabihf, repo: "hexpm", optional: false]}], "hexpm"},
"fs": {:hex, :fs, "3.4.0", "6d18575c250b415b3cad559e6f97a4c822516c7bc2c10bfbb2493a8f230f5132", [:rebar3], [], "hexpm"},
"fwup": {:hex, :fwup, "0.3.0", "2c360815565fcbc945ebbb34b58f156efacb7f8d64766f1cb3426919bb3f41ea", [:mix], [], "hexpm"},
"gen_stage": {:hex, :gen_stage, "0.14.0", "65ae78509f85b59d360690ce3378d5096c3130a0694bab95b0c4ae66f3008fad", [:mix], [], "hexpm"},
"gettext": {:hex, :gettext, "0.16.0", "4a7e90408cef5f1bf57c5a39e2db8c372a906031cc9b1466e963101cb927dafc", [:mix], [], "hexpm"},
"goldrush": {:hex, :goldrush, "0.1.9", "f06e5d5f1277da5c413e84d5a2924174182fb108dabb39d5ec548b27424cd106", [:rebar3], [], "hexpm"},
"hackney": {:hex, :hackney, "1.14.3", "b5f6f5dcc4f1fba340762738759209e21914516df6be440d85772542d4a5e412", [:rebar3], [{:certifi, "2.4.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
"httpoison": {:hex, :httpoison, "1.3.1", "7ac607311f5f706b44e8b3fab736d0737f2f62a31910ccd9afe7227b43edb7f0", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
"jason": {:hex, :jason, "1.1.1", "d3ccb840dfb06f2f90a6d335b536dd074db748b3e7f5b11ab61d239506585eb2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
"joken": {:hex, :joken, "1.5.0", "42a0953e80bd933fc98a0874e156771f78bf0e92abe6c3a9c22feb6da28efb0b", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
"jose": {:hex, :jose, "1.8.4", "7946d1e5c03a76ac9ef42a6e6a20001d35987afd68c2107bcd8f01a84e75aa73", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"},
"jsx": {:hex, :jsx, "2.9.0", "d2f6e5f069c00266cad52fb15d87c428579ea4d7d73a33669e12679e203329dd", [:mix, :rebar3], [], "hexpm"},
@ -42,9 +43,11 @@
"mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"},
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"},
"muontrap": {:hex, :muontrap, "0.4.0", "f3c48f5e2cbb89b6406d28e488fbd0da1ce0ca00af332860913999befca9688a", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"},
"nerves": {:hex, :nerves, "1.3.1", "4e2002315b38d38c6bfb5d3e33d7f4a32057037c3f2026af64d0867bc1741b21", [:mix], [{:distillery, "~> 2.0", [hex: :distillery, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
"nerves": {:hex, :nerves, "1.3.4", "9523cc1936f173c99cf15a132c2b24f9c6f1a5cfe3327bbcd518ff7e441327d3", [:mix], [{:distillery, "2.0.10", [hex: :distillery, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
"nerves_firmware": {:hex, :nerves_firmware, "0.4.0", "ac2fed915a7ca4bb69f567d9b742d77cffc3a6a56420ce65e870c8c34119b935", [:mix], [], "hexpm"},
"nerves_firmware_ssh": {:hex, :nerves_firmware_ssh, "0.3.3", "79c42303ddbfd89ae6f5b4b19a4397a6188df21ca0e7a6573c2399e081fb7d25", [:mix], [{:nerves_runtime, "~> 0.4", [hex: :nerves_runtime, repo: "hexpm", optional: false]}], "hexpm"},
"nerves_hub": {:git, "https://github.com/nerves-hub/nerves_hub.git", "2da9a6fea2b53fa9e0251045b06e265e21847d24", []},
"nerves_hub_cli": {:hex, :nerves_hub_cli, "0.4.0", "d5efcc49179fff8f3cd4542831820082d6abf2290fb5251c85e54ea51614b35d", [:mix], [{:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:pbcs, "~> 0.1", [hex: :pbcs, repo: "hexpm", optional: false]}, {:tesla, "~> 1.2.1 or ~> 1.3", [hex: :tesla, repo: "hexpm", optional: false]}, {:x509, "~> 0.3", [hex: :x509, repo: "hexpm", optional: false]}], "hexpm"},
"nerves_init_gadget": {:hex, :nerves_init_gadget, "0.5.1", "07f3eeb9acb3f919b3b34b36f552bb38d70f2d29ace63f3f23f33eee6a1ca693", [:mix], [{:mdns, "~> 1.0", [hex: :mdns, repo: "hexpm", optional: false]}, {:nerves_firmware_ssh, "~> 0.2", [hex: :nerves_firmware_ssh, repo: "hexpm", optional: false]}, {:nerves_network, "~> 0.3", [hex: :nerves_network, repo: "hexpm", optional: false]}, {:nerves_runtime, "~> 0.3", [hex: :nerves_runtime, repo: "hexpm", optional: false]}, {:one_dhcpd, "~> 0.1", [hex: :one_dhcpd, repo: "hexpm", optional: false]}, {:ring_logger, "~> 0.4", [hex: :ring_logger, repo: "hexpm", optional: false]}], "hexpm"},
"nerves_leds": {:hex, :nerves_leds, "0.8.0", "193692767dca1a201b09113d242648493b9be0087bab83ebee99c3b0a254f5e1", [:mix], [], "hexpm"},
"nerves_network": {:hex, :nerves_network, "0.3.7", "200767191b1ded5a61cddbacd3efdce92442cc055bdc37c20ca8c7cb1d964098", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nerves_network_interface, "~> 0.4.4", [hex: :nerves_network_interface, repo: "hexpm", optional: false]}, {:nerves_wpa_supplicant, "~> 0.3", [hex: :nerves_wpa_supplicant, repo: "hexpm", optional: false]}, {:system_registry, "~> 0.7", [hex: :system_registry, repo: "hexpm", optional: false]}], "hexpm"},
@ -61,6 +64,8 @@
"net_logger": {:hex, :net_logger, "0.1.0", "59be302c09cf70dab164810c923ccb9a976eda7270e5a32b93ba8aeb850de1d6", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:uuid, "~> 1.1", [hex: :uuid, repo: "hexpm", optional: false]}], "hexpm"},
"one_dhcpd": {:hex, :one_dhcpd, "0.2.0", "18eb8ce7101ad7b79e67f3d7ee7f648f42e02b8fa4c1cb3f24f403bf6860f81d", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"},
"pbcs": {:hex, :pbcs, "0.1.0", "6f79ce81d93edf5ac41fcd8b32fb203ad6895ebdb33d115e14a5bd955b90020a", [:mix], [], "hexpm"},
"phoenix_channel_client": {:hex, :phoenix_channel_client, "0.3.2", "188f6e4cad20da03e04f685416a86f682c413efca7c72303f479b94662cb897c", [:mix], [{:websocket_client, "~> 1.3", [hex: :websocket_client, repo: "hexpm", optional: true]}], "hexpm"},
"phoenix_html": {:hex, :phoenix_html, "2.12.0", "1fb3c2e48b4b66d75564d8d63df6d53655469216d6b553e7e14ced2b46f97622", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"plug": {:hex, :plug, "1.6.4", "35618dd2cc009b69b000f785452f6b370f76d099ece199733fea27bc473f809d", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"},
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
@ -78,8 +83,11 @@
"sqlitex": {:hex, :sqlitex, "1.4.3", "a50f12d6aeb25f4ebb128453386c09bbba8f5abd3c7713dc5eaa92f359926ac5", [:mix], [{:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:esqlite, "~> 0.2.4", [hex: :esqlite, repo: "hexpm", optional: false]}], "hexpm"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"},
"system_registry": {:hex, :system_registry, "0.8.0", "09240347628b001433d18279a2759ef7237ba7361239890d8c599cca9a2fbbc2", [:mix], [], "hexpm"},
"tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
"timex": {:hex, :timex, "3.4.1", "e63fc1a37453035e534c3febfe9b6b9e18583ec7b37fd9c390efdef97397d70b", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
"tzdata": {:hex, :tzdata, "0.5.19", "7962a3997bf06303b7d1772988ede22260f3dae1bf897408ebdac2b4435f4e6a", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"},
"uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm"},
"websocket_client": {:hex, :websocket_client, "1.3.0", "2275d7daaa1cdacebf2068891c9844b15f4fdc3de3ec2602420c2fb486db59b6", [:rebar3], [], "hexpm"},
"x509": {:hex, :x509, "0.3.0", "c6f3db66960c6e4f424d1e6cca5c7d730e0a577af8dc115a613f4560ce1df6d3", [:mix], [], "hexpm"},
}

View File

@ -1,7 +0,0 @@
defmodule Nerves.Firmware do
@moduledoc false
@doc false
def upgrade_and_finalize(_), do: :ok
def reboot, do: :ok
end

View File

@ -0,0 +1,27 @@
defmodule Farmbot.Host.NervesHubHandler do
@behaviour Farmbot.System.NervesHub
def serial_number do
{:ok, [_ | [{_ifname, info} | _]]} = :inet.getifaddrs()
:io_lib.format('~2.16.0B~2.16.0B~2.16.0B~2.16.0B~2.16.0B~2.16.0B', info[:hwaddr])
|> to_string()
|> String.trim()
end
def connect, do: :ok
def provision(_serial), do: :ok
def configure_certs(_cert, _key), do: :ok
def deconfigure, do: :ok
def config, do: [
serial_number(),
serial_number(),
"Not a real cert",
"Not a real key"
]
def check_update, do: nil
end

View File

@ -1,26 +0,0 @@
defmodule Farmbot.Host.UpdateHandler do
@moduledoc false
@behaviour Farmbot.System.UpdateHandler
use Farmbot.Logger
# Update Handler callbacks
def apply_firmware(_file_path) do
:ok
end
def before_update do
:ok
end
def post_update do
:ok
end
def setup(_env) do
:ok
end
def requires_reboot?, do: false
end

View File

@ -0,0 +1,119 @@
defmodule Farmbot.System.NervesHubClient do
@moduledoc """
Client that decides when an update should be done.
"""
use GenServer
require Logger
@behaviour NervesHub.Client
@behaviour Farmbot.System.NervesHub
import Farmbot.System.ConfigStorage, only: [get_config_value: 3]
def serial_number("rpi0"), do: serial_number("rpi")
def serial_number("rpi3"), do: serial_number("rpi")
def serial_number(plat) do
:os.cmd('/usr/bin/boardid -b uboot_env -u nerves_serial_number -b uboot_env -u serial_number -b #{plat}')
|> to_string()
|> String.trim()
end
def serial_number, do: serial_number(Farmbot.Project.target())
def connect do
Logger.info "Starting NervesHub app."
# NervesHub replaces it's own env on startup. Reset it.
Application.put_env(:nerves_hub, NervesHub.Socket, [reconnect_interval: 5000])
# Stop Nerves Hub if it is running.
_ = Application.stop(:nerves_hub)
# Cause NervesRuntime.KV to restart.
_ = GenServer.stop(Nerves.Runtime.KV)
{:ok, _} = Application.ensure_all_started(:nerves_hub)
# Wait for a few seconds for good luck.
Process.sleep(1000)
r = NervesHub.connect()
Logger.info "NervesHub started: #{inspect(r, limit: :infinity)}"
:ok
end
def provision(serial) do
Nerves.Runtime.KV.UBootEnv.put("nerves_serial_number", serial)
Nerves.Runtime.KV.UBootEnv.put("nerves_fw_serial_number", serial)
end
def configure_certs(cert, key) do
Nerves.Runtime.KV.UBootEnv.put("nerves_hub_cert", cert)
Nerves.Runtime.KV.UBootEnv.put("nerves_hub_key", key)
:ok
end
def deconfigure() do
Nerves.Runtime.KV.UBootEnv.put("nerves_hub_cert", "")
Nerves.Runtime.KV.UBootEnv.put("nerves_hub_key", "")
Nerves.Runtime.KV.UBootEnv.put("nerves_serial_number", "")
Nerves.Runtime.KV.UBootEnv.put("nerves_fw_serial_number", "")
:ok
end
def config() do
[
Nerves.Runtime.KV.get("nerves_fw_serial_number"),
Nerves.Runtime.KV.get("nerves_hub_cert"),
Nerves.Runtime.KV.get("nerves_hub_key"),
]
end
def check_update do
case GenServer.call(__MODULE__, :check_update) do
# If updates were disabled, and an update is queued
{:ignore, _url} -> NervesHub.update()
_ -> nil
end
end
# Callback for NervesHub.Client
def update_available(args) do
GenServer.call(__MODULE__, {:update_available, args}, :infinity)
end
def handle_fwup_message({:progress, percent}) do
Logger.info("FWUP Stream Progress: #{percent}%")
alias Farmbot.BotState.JobProgress
prog = %JobProgress.Percent{percent: percent}
if Process.whereis(Farmbot.BotState) do
Farmbot.BotState.set_job_progress("FBOS_OTA", prog)
end
:ok
end
def handle_fwup_message({:error, _, reason}) do
Logger.error "FWUP Error: #{reason}"
:ok
end
def handle_fwup_message(_) do
:ok
end
def start_link(_, _) do
GenServer.start_link(__MODULE__, [], [name: __MODULE__])
end
def init([]) do
{:ok, nil}
end
def handle_call({:update_available, %{"firmware_url" => url}}, _, _state) do
if Process.whereis(Farmbot.BotState) do
Farmbot.BotState.set_update_available(true)
end
case get_config_value(:bool, "settings", "os_auto_update") do
true -> {:reply, :apply, {:apply, url}}
false -> {:reply, :ignore, {:ignore, url}}
end
end
def handle_call(:check_update, _from, state) do
{:reply, state, state}
end
end

View File

@ -28,7 +28,6 @@ defmodule Farmbot.Target.Uevent do
end
def handle_info({:system_registry, :global, new_reg}, %{} = old_reg) do
require IEx; IEx.pry
new_ttys = get_in(new_reg, [:state, "subsystems", "tty"]) || []
old_ttys = get_in(old_reg, [:state, "subsystems", "tty"]) || []
case new_ttys -- old_ttys do

View File

@ -1,66 +0,0 @@
defmodule Farmbot.Target.UpdateHandler do
@moduledoc "Handles prep and post OTA update."
@behaviour Farmbot.System.UpdateHandler
use Farmbot.Logger
# Update Handler callbacks
def apply_firmware(fw_file_path) do
{meta_bin, 0} = System.cmd("fwup", ~w"-i #{fw_file_path} -m")
meta_bin
|> String.trim()
|> String.split("\n")
|> Enum.map(&String.split(&1, "="))
|> Map.new(fn([key, val]) ->
{key, val |> String.trim_leading("\"") |> String.trim_trailing("\"")}
end)
|> log_meta()
Nerves.Firmware.upgrade_and_finalize(fw_file_path)
end
defp log_meta(meta_map) do
target = "target: #{meta_map["meta-platform"]}"
product = "product: #{meta_map["meta-product"]}"
version = "version: #{meta_map["meta-version"]}"
create_time = "created: #{meta_map["meta-creation-date"]}"
msg = """
Applying Firmware:
#{create_time}
#{target}
#{product}
#{version}
"""
Logger.debug 1, msg
end
def before_update, do: :ok
def post_update do
alias Farmbot.Firmware.UartHandler.Update
hw = Farmbot.System.ConfigStorage.get_config_value(:string, "settings", "firmware_hardware")
is_beta? = Farmbot.System.ConfigStorage.get_config_value(:bool, "settings", "currently_on_beta")
if is_beta? do
Logger.debug 1, "Forcing beta image arduino firmware flash."
Update.force_update_firmware(hw)
else
Update.maybe_update_firmware(hw)
end
:ok
end
def setup(:prod) do
file = "#{:code.priv_dir(:farmbot)}/fwup-key.pub"
Application.put_env(:nerves_firmware, :pub_key_path, file)
if File.exists?(file), do: :ok, else: {:error, :no_pub_file}
end
def setup(_) do
:ok
end
def requires_reboot? do
!Nerves.Firmware.allow_upgrade?
end
end

File diff suppressed because it is too large Load Diff

2658
priv/farmduino.hex 100644

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1
priv/prod.pub 100644
View File

@ -0,0 +1 @@
ZL9J9sLK3GWa7YezB3v41Kxl0TmE15CARjApOGcNa1c=

1
priv/staging.pub 100644
View File

@ -0,0 +1 @@
qIbhsGnUVHGEu++RAss2U0caXySSwDBVKkK35sO0vVU=

View File

@ -1,4 +1,6 @@
-setcookie democookie
+Bc
-kernel shell_history enabled
-mode embedded
-sname farmbot
-noshell

View File

@ -1,7 +1,6 @@
#!/bin/bash
TARGETS="rpi0 \
rpi3 \
bbb \
host
"
for target in $TARGETS; do

View File

@ -1,19 +0,0 @@
defmodule Farmbot.System.UpdateTimerTest do
use ExUnit.Case, async: false
alias Farmbot.System.ConfigStorage
test "Opting into beta updates should refresh token" do
Farmbot.System.Registry.subscribe(self())
old = ConfigStorage.get_config_value(:string, "authorization", "token")
ConfigStorage.update_config_value(:bool, "settings", "beta_opt_in", false)
ConfigStorage.update_config_value(:bool, "settings", "beta_opt_in", true)
assert_receive {Farmbot.System.Registry, {:config_storage, {"settings", "beta_opt_in", true}}}
assert_receive {Farmbot.System.Registry, {:authorization, :new_token}}, 1000
new = ConfigStorage.get_config_value(:string, "authorization", "token")
assert old != new
end
end

View File

@ -1,108 +0,0 @@
defmodule Farmbot.System.UpdatesTest do
use ExUnit.Case, async: false
alias Farmbot.System.Updates
alias Farmbot.System.Updates.{Release, CurrentStuff}
@old_version Farmbot.Project.version |> Version.parse! |> Map.update(:major, nil, &Kernel.-(&1, 1)) |> to_string()
@new_version Farmbot.Project.version |> Version.parse! |> Map.update(:major, nil, &Kernel.+(&1, 1)) |> to_string()
@commit Farmbot.Project.commit
@version Farmbot.Project.version()
@os_update_server "http://fake_os_update_server.com"
@beta_os_update_server "http://beta_os_update_server.com"
@fake_asset_url "http://fake_release_asset.com"
describe "CurrentStuff replacement" do
test "replaces valid things in the current stuff struct" do
r = CurrentStuff.get(env: :almost_prod)
assert r.env == :almost_prod
end
test "Allows and igrnores arbitry data" do
r = CurrentStuff.get(some_key: :some_val)
refute Map.get(r, :some_key)
end
end
@tag :external
test "checks for updates for prod rpi3 no beta combo" do
# updating from old to new version should work
current = CurrentStuff.get(os_update_server_overwrite: nil, beta_opt_in: false, env: :prod, target: :rpi3, version: @old_version)
assert Updates.check_updates(nil, current)
end
@tag :external
test "checks for updates for prod rpi3 with beta combo" do
# old version to later beta
current = CurrentStuff.get(os_update_server_overwrite: nil, env: :prod, target: :rpi3, version: @old_version, beta_opt_in: true)
assert Updates.check_updates(nil, current)
end
test "no token gives error" do
current = CurrentStuff.get(token: nil)
assert match?({:error, _}, Updates.check_updates(nil, current))
end
test "dev env should not update to prod" do
current = CurrentStuff.get(env: :dev)
assert match?({:error, _}, Updates.check_updates(nil, current))
end
test "updates of the same version should not return a url" do
current = CurrentStuff.get(current_stub())
release = release_stub()
refute Updates.check_updates(release, current)
end
test "Draft releases" do
current = CurrentStuff.get(current_stub())
release = %{release_stub() | draft: true}
refute Updates.check_updates(release, current)
end
test "Opting into beta won't downgrade from a prod release to a previous beta" do
current = CurrentStuff.get(%{current_stub() | beta_opt_in: true, version: @new_version})
release = release_stub()
refute Updates.check_updates(release, current)
end
test "Normal upgrade path: current is less than latest" do
current = CurrentStuff.get(%{current_stub() | version: @old_version})
release = release_stub()
assert match?({%Version{}, @fake_asset_url}, Updates.check_updates(release, current))
end
test "versions equal, but commits not equal" do
current = CurrentStuff.get(%{current_stub() | commit: String.reverse(@commit)})
release = release_stub()
assert match?({%Version{}, @fake_asset_url}, Updates.check_updates(release, current))
end
defp current_stub do
%{
token: %Farmbot.Jwt{
os_update_server: @os_update_server,
beta_os_update_server: @beta_os_update_server,
},
beta_opt_in: false,
currently_on_beta: false,
os_update_server_overwrite: nil,
env: :prod,
commit: @commit,
target: :rpi3,
version: @version
}
end
defp release_stub do
%Release{
tag_name: "v#{@version}",
target_commitish: @commit,
name: "Stub Release",
draft: false,
prerelease: false,
body: "This is a stub!",
assets: [%Release.Asset{name: "farmbot-rpi3-#{@version}.fw", browser_download_url: @fake_asset_url}]
}
end
end

View File

@ -1,26 +0,0 @@
defmodule FarmbotTestSupport.TestUpdateHandler do
@moduledoc "Handles prep and post OTA update."
@behaviour Farmbot.System.UpdateHandler
use Farmbot.Logger
# Update Handler callbacks
def apply_firmware(_file_path) do
:ok
end
def before_update do
:ok
end
def post_update do
:ok
end
def setup(_env) do
:ok
end
def requires_reboot?, do: false
end