Compare commits

..

14 Commits

Author SHA1 Message Date
Connor Rigby 3886304c54
Force farmware to be reinstalled.
This has to be done because v8 was shipped with a configuration error causing
farmware to install to the temporary data partition on the raspberry pi.
2019-12-31 10:32:27 -08:00
Connor Rigby bd51878b41
Force bootupsequence worker to check farmware before executing 2019-12-31 10:32:27 -08:00
connor rigby 3f06afdccb
Make bootup sequence wait for farmware installion 2019-12-31 10:32:27 -08:00
connor rigby f23eb81616
Update bootup sequence logs 2019-12-31 10:32:27 -08:00
Connor Rigby 503e62507e
Refactor bootup sequence to be in `farmbot_os` application
This moves it's execution to much later in the boot process. Doing this
allows the bootup sequence to check for a number of things before
executing. Checked systems are:

* firmware configured
* bot is synced (which happens on every boot now)
* peripherals loaded
2019-12-31 10:32:26 -08:00
Connor Rigby e79e1e4d05
Add bootup sequence selction log 2019-12-31 10:32:09 -08:00
Connor Rigby 6704a40c5a
Force farmbot to sync on first boot 2019-12-31 10:32:09 -08:00
Connor Rigby fd7510fc94
Add bootup sequence feature 2019-12-31 10:32:08 -08:00
connor rigby 10f3fddb9a
Remove home_on_boot from FarmbotFirmware
This feature is now encompased in the new Boot Sequence feature
2019-12-31 10:32:08 -08:00
connor rigby 9b9091d5e2
Add `boot_sequence_id` to FbosConfig
also force resync of fbos config
2019-12-31 10:32:08 -08:00
Connor Rigby 1a1bcb49bc
Update fbos_config and peripherals to coordinate with firmware status 2019-12-31 10:32:08 -08:00
Connor Rigby 12049e04ba
Add helpers for inspecting asset states 2019-12-31 10:32:08 -08:00
Connor Rigby a76f67347a
Add new `configured` state to farmbot firmware 2019-12-31 10:32:07 -08:00
Connor Rigby d34ab2abb2
Add new system to track dependency statuses globally 2019-12-31 10:31:50 -08:00
300 changed files with 18369 additions and 15134 deletions

View File

@ -2,7 +2,7 @@ version: 2.0
defaults: &defaults
working_directory: /nerves/build
docker:
- image: nervesproject/nerves_system_br:1.11.3
- image: nervesproject/nerves_system_br:latest
install_elixir: &install_elixir
run:
@ -162,9 +162,7 @@ deploy_nerves_hub_firmware_steps: &deploy_nerves_hub_firmware_steps
- run:
name: Sign Image
working_directory: /nerves/build/farmbot_os
command: |
mix deps.get
mix nerves_hub.firmware sign --key notreal /nerves/deploy/system/artifacts/farmbot-${MIX_TARGET}-$(cat ../VERSION).fw
command: mix nerves_hub.firmware sign --key notreal /nerves/deploy/system/artifacts/farmbot-${MIX_TARGET}-$(cat ../VERSION).fw
- run:
name: Publish to NervesHub
working_directory: /nerves/build/farmbot_os
@ -201,13 +199,12 @@ jobs:
mix compile
mix format --check-formatted
mix coveralls.json
bash <(curl -s https://codecov.io/bash)
- save_cache:
key: v14-fbcs-test-dependency-cache-{{ checksum "farmbot_celery_script/mix.lock" }}
paths:
- farmbot_celery_script/_build/test
- farmbot_celery_script/deps
- save_cache:
- save_cache:
key: v14-fbcs-coverage-cache-{{ .Branch }}-{{ .Revision }}
paths:
- farmbot_celery_script/cover
@ -236,13 +233,12 @@ jobs:
mix compile
mix format --check-formatted
mix coveralls.json
bash <(curl -s https://codecov.io/bash)
- save_cache:
key: v14-fbfw-test-dependency-cache-{{ checksum "farmbot_firmware/mix.lock" }}
paths:
- farmbot_firmware/_build/test
- farmbot_firmware/deps
- save_cache:
- save_cache:
key: v14-fbfw-coverage-cache-{{ .Branch }}-{{ .Revision }}
paths:
- farmbot_firmware/cover
@ -281,14 +277,13 @@ jobs:
mix compile
mix format --check-formatted
mix coveralls.json --trace
bash <(curl -s https://codecov.io/bash)
- save_cache:
key: v14-fbcore-test-dependency-cache-{{ checksum "farmbot_core/mix.lock" }}
paths:
- farmbot_core/_build/test
- farmbot_core/deps
- farmbot_core/arduino
- save_cache:
- save_cache:
key: v14-fbcore-coverage-cache-{{ .Branch }}-{{ .Revision }}
paths:
- farmbot_core/cover
@ -330,13 +325,12 @@ jobs:
mix ecto.create
mix ecto.migrate
mix coveralls.json
bash <(curl -s https://codecov.io/bash)
- save_cache:
key: v14-fbext-test-dependency-cache-{{ checksum "farmbot_ext/mix.lock" }}
paths:
- farmbot_ext/_build/test
- farmbot_ext/deps
- save_cache:
- save_cache:
key: v14-fbext-coverage-cache-{{ .Branch }}-{{ .Revision }}
paths:
- farmbot_ext/cover
@ -354,7 +348,7 @@ jobs:
- checkout
- run: git submodule update --init --recursive
- restore_cache:
keys:
keys:
- v14-fbos-host-test-dependency-cache-{{ checksum "farmbot_os/mix.lock" }}
- <<: *install_elixir
- <<: *install_hex_archives
@ -366,18 +360,69 @@ jobs:
mix compile
mix format --check-formatted
mix coveralls.json
bash <(curl -s https://codecov.io/bash)
- save_cache:
key: v14-fbos-host-test-dependency-cache-{{ checksum "farmbot_os/mix.lock" }}
paths:
- farmbot_os/_build/host
- farmbot_os/deps/host
- save_cache:
- save_cache:
key: v14-fbos-coverage-cache-{{ .Branch }}-{{ .Revision }}
paths:
- farmbot_os/cover
- store_artifacts:
path: farmbot_os/cover
report_coverage:
<<: *defaults
environment:
MIX_ENV: test
MIX_TARGET: host
NERVES_LOG_DISABLE_PROGRESS_BAR: "yes"
ELIXIR_VERSION: 1.8.0
steps:
- checkout
- run: git submodule update --init --recursive
- <<: *install_elixir
- <<: *install_hex_archives
- <<: *install_mdl
- restore_cache:
keys:
- v14-fbsupport-test-dependency-cache-{{ checksum "mix.lock" }}
- restore_cache:
keys:
- v14-fbcs-coverage-cache-{{ .Branch }}-{{ .Revision }}
- restore_cache:
keys:
- v14-fbcs-coverage-cache-{{ .Branch }}-{{ .Revision }}
- restore_cache:
keys:
- v14-fbfw-coverage-cache-{{ .Branch }}-{{ .Revision }}
- restore_cache:
keys:
- v14-fbcore-coverage-cache-{{ .Branch }}-{{ .Revision }}
- restore_cache:
keys:
- v14-fbext-coverage-cache-{{ .Branch }}-{{ .Revision }}
- restore_cache:
keys:
- v14-fbos-coverage-cache-{{ .Branch }}-{{ .Revision }}
- run:
name: Check documentation formatting
command: |
mdl docs/
- run:
name: Report Coverage
working_directory: /nerves/build/
command: |
mix deps.get
mix compile
mix format --check-formatted
mix farmbot.coveralls circle
- save_cache:
key: v14-fbsupport-test-dependency-cache-{{ checksum "mix.lock" }}
paths:
- deps/
- _build
################################################################################
# target=rpi app_env=prod #
@ -672,6 +717,22 @@ workflows:
- beta
- next
- /^qa\/.*/
- report_coverage:
context: org-global
requires:
- test_farmbot_celery_script
- test_farmbot_firmware
- test_farmbot_core
- test_farmbot_ext
- test_farmbot_os
filters:
branches:
ignore:
- master
- staging
- beta
- next
- /^qa\/.*/
# master branch to staging.farmbot.io
nerves_hub_prod_stable_staging:

View File

@ -1,77 +1,7 @@
# Changelog
# 10.0.0
* Deprecate `resource_update` RPC
* Introduce `update_resource` RPC, which allows users to modify variables from the sequence editor.
* Genesis v1.5 and Express v1.0 firmware updates.
* Fix a bug where FBOS would not honor an "AUTO UPDATE" value of "false".
# 9.2.2
* Fix firmware locking error ("Can't perform X in Y state")
* Removal of dead code / legacy plus numerous unit test additions.
* Added coveralls test coverage reporter
* Unit test additions (+2.7% coverage :tada:)
* Updates to build instructions for third party developers
* Bug fix for criteria-based groups that have only one filter criteria.
* Bug fix for express bots involving timeout during remote firmware flash
* Remove VCR again (for now)
* Increase farmware timeout to 20 minutes (use at own risk)
# 9.2.1
* Improve firmware debug messages.
* Remove confusing firmware debug messages, such as "Error OK".
* Improved camera support on FarmBot express.
* Bug fix to prevents OTA updates occuring when one is already in progress.
# 9.2.0
* Support for criteria-based groups.
* Genesis v1.5 and Express v1.0 firmware homing updates.
* Fix bug where unknown positions would report as -1.
# 9.1.2
* Genesis v1.5 and Express v1.0 firmware updates.
* Bug fix for movement error reporting
* Improved firmware error message reporting
* Improved support for gantry mounted tools.
# 9.1.1
* Genesis v1.5 and Express v1.0 firmware updates.
# 9.1.0
* Improved support for new FarmBot Express models
* Various firmware bug fixes for Express models.
* Bug fix for slow Farmware execution (Thanks, @jsimmonds2)
* Dependency upgrades
* Upgrade VintageNet (networking library)
* Removal of `dump_info` RPCs
* Numerous internal improvements, such as increasing test coverage and changing dependency injection scheme.
* Fix issue where firmware commands would be tried too many times.
# 9.0.4
* Bug fix for slow Farmware execution (Thanks, @jsimmonds2)
* Dependency upgrades
* Upgrade VintageNet (networking library)
* Removal of `dump_info` RPCs
* Numerous internal improvements, such as increasing test coverage and changing dependency injection scheme.
# 9.0.3
* Dependency updates
# 9.0.2
* See notes for 9.0.1.
# 9.0.1
* Routine token updates on Circle CI.
* Fix bugs that were causing devices to erroneously factory reset under some circumstances.
# 9.0.0
* Run updates on Nerves systems.

View File

@ -1,10 +0,0 @@
# Jan - Mar 2020
| Project | Jan 1 20 | Feb 6 20 | Mar 4 20 |STATUS|
|-----------------------|----------|----------|----------|------|
| farmbot_celery_script | 53.7% | 54.0% | 54.0% |OK |
| farmbot_core | 22.2% | 19.8% | 26.3% |OK |
| farmbot_ext | 53.6% | 52.7% | 38.1% |FIX | !!!
| farmbot_firmware | 13.8% | 56.4% | 62.0% |OK |
| farmbot_os | 22.0% | 27.6% | 45.3% |OK |
| farmbot_telemetry | ??.?% | ??.?% | ??.?% |LATER |

View File

@ -5,7 +5,6 @@
"assertion_block": "8.0.0",
"backscheduled_regimens": "6.4.0",
"change_ownership": "6.3.0",
"criteria_groups": "9.2.2",
"diagnostic_dumps": "6.4.4",
"endstop_invert": "6.4.1",
"express_k10": "8.0.0",
@ -21,7 +20,6 @@
"ota_update_hour": "8.2.3",
"rpi_led_control": "6.4.4",
"sensors": "6.3.0",
"update_resource": "10.0.0",
"use_update_channel": "6.4.12",
"variables": "8.0.0"
}

View File

@ -1,17 +1,19 @@
# Build status
| Master Build Status | Staging Build Status |
| :---: | :---: |
| [![Master Build Status](https://circleci.com/gh/FarmBot/farmbot_os/tree/master.svg?style=svg)](https://circleci.com/gh/FarmBot/farmbot_os/tree/master) | [![Staging Build Status](https://circleci.com/gh/FarmBot/farmbot_os/tree/staging.svg?style=svg)](https://circleci.com/gh/FarmBot/farmbot_os/tree/staging) |
---
<!-- DON'T CHANGE THE TEXT BELOW. It is used in documentation links. -->
# :floppy_disk: Latest OS Image Downloads
<!-- DON'T CHANGE THE TEXT ABOVE. It is used in documentation links. -->
The FarmBot OS release page has moved to [my.farm.bot/os](https://my.farm.bot/os)
Download the version of FarmBot OS that corresponds to the FarmBot kit and computer you have:
Old versions of FarmBot OS can be found [here](https://github.com/FarmBot/farmbot_os/releases). Please note that [FarmBot does not provide support for old versions of FBOS](https://software.farm.bot/docs/support-policy).
---
## Build status
| Master Build Status | Staging Build Status | Test Coverage |
| :---: | :---: | :---: |
| [![Master Build Status](https://circleci.com/gh/FarmBot/farmbot_os/tree/master.svg?style=svg)](https://circleci.com/gh/FarmBot/farmbot_os/tree/master) | [![Staging Build Status](https://circleci.com/gh/FarmBot/farmbot_os/tree/staging.svg?style=svg)](https://circleci.com/gh/FarmBot/farmbot_os/tree/staging) | [![codecov](https://codecov.io/gh/FarmBot/farmbot_os/branch/staging/graph/badge.svg)](https://codecov.io/gh/FarmBot/farmbot_os) |
| FarmBot Kit | Computer | Download Link |
| --- | --- | --- |
| Genesis v1.2, Genesis v1.3, Genesis v1.4, Genesis XL v1.4 | Raspberry Pi 3 | [Download FBOS](https://github.com/FarmBot/farmbot_os/releases/download/v9.0.0/farmbot-rpi3-9.0.0.img) |
| Express v1.0, Express XL v1.0 | Raspberry Pi Zero W | Coming soon |
---
@ -33,8 +35,6 @@ Installation should be fairly straight forward, you will need a computer for thi
1. download the [latest release file, available for download here](#floppy_disk-latest-os-image-downloads).
0. ```dd if=</path/to/file> of=/dev/<sddevice> bs=4``` or use [Etcher](https://etcher.io/).
---
## Running
_Refer to the [software documentation Configurator page](https://software.farm.bot/docs/configurator) for more detailed instructions._

View File

@ -24,18 +24,3 @@ This release uses an improved Farmware API:
* If you are a Farmware developer using Farmware Tools (`import farmware_tools`), the reinstalled Farmware should continue working as before. If you have authored a Farmware that does not use the package, you will need to replace any FarmBot device communication in your Farmware to use the `farmware_tools` package.
* See the [Farmware developer documentation](https://developer.farm.bot/docs/farmware) for more information.
# v9
FarmBot OS v8+ uses an improved Farmware API. See the [Farmware developer documentation](https://developer.farm.bot/docs/farmware) for more information.
# v10
FarmBot OS v10 features an improved *Mark As* step. If you have previously added *Mark As* steps to sequences, you will need to update them before they can be executed by FarmBot:
* Open any sequences with a caution icon next to the name.
* Click the `CONVERT` button in each old *Mark As* step.
* Save the sequence.
* If you have auto-sync disabled, press `SYNC NOW` once all sequences have been updated.
* Verify that any events using the updated sequences are running as expected.
FarmBot OS auto-update was disabled prior to this release.

View File

@ -1 +1 @@
10.0.0
9.0.1-rc1

View File

@ -6,30 +6,6 @@ This document will act as an index to available documentation.
* [FarmBot Source Code common terms](/docs/glossary.md)
## Cheat Sheet
**Create a *.fw file from local repo (RPi Zero):**
```sh
NERVES_SYSTEM=farmbot_system_rpi MIX_TARGET=rpi mix deps.get
NERVES_SYSTEM=farmbot_system_rpi MIX_TARGET=rpi mix firmware
sudo fwup farmbot_os/_build/rpi/rpi_dev/nerves/images/farmbot.fw
```
**Create a *.fw file from local repo (RPi v3):**
```sh
NERVES_SYSTEM=farmbot_system_rpi3 MIX_TARGET=rpi3 mix deps.get
NERVES_SYSTEM=farmbot_system_rpi3 MIX_TARGET=rpi3 mix firmware
sudo fwup farmbot_os/_build/rpi3/rpi3_dev/nerves/images/farmbot.fw
```
**Create or Update the Nerves System:**
Please see the official [Nerves documentation on "Nerves Systems"](https://hexdocs.pm/nerves/0.4.0/systems.html).
HINT: You may want to [develop the system locally](https://stackoverflow.com/a/28189056/1064917)
## Hardware specifics
Most FarmBot development/testing is done on a standard desktop PC.

View File

@ -10,7 +10,7 @@ Publishing a FarmBotOS release requires coordination of a few different systems.
## Legacy Release System
The legacy system is somewhat simpler. It goes as follows:
The legacy system is somewhat simpiler. It goes as follows:
### Pull request into `master` branch
@ -54,7 +54,7 @@ Beta releases are constructed by creating a tag off of the `staging` branch.
## NervesHub System
The NervesHub system is simpler to use, but more complex to setup.
The NervesHub system is simpiler to use, but more complex to setup.
### User registration

View File

@ -1,69 +1,14 @@
# Publishing OTAs
## Beta OTA channel
## Beta
Beta updates are simply tags matching the following semver string:
```
vMajor.Minor.Tiny-rcRC
```
for example:
```
v10.5.6-rc30
```
To publish an OTA, just tag a release matching that
string.
Publish an OTA to the `beta` channel can be done by:
```bash
cd $FARMBOT_OS_ROOT_DIRECTORY
git checkout staging
# Ensure you don't accidentally publish local changes
# that have not gone through CI:
git fetch --all
git reset --hard origin/staging
# update the CHANGELOG, but DO NOT put the `rc`
# on the semver string.
$EDITOR CHANGELOG.md
echo 1.2.3-rc4 > VERSION
git add -A
git commit -am "Release v10.5.6-rc30"
git tag v1.2.3-rc4
git push origin v1.2.3-rc4
```
or call the helper script:
`./scripts/release_candidate.sh`
### NOTE about release candidate script
the helper script only **increments** the
RC version. Calling the `release-candidate` script
from a non rc version will fail. Example:
This will fail:
```bash
$ cat VERSION
10.5.6
./scripts/release_candidate.sh
```
This will succeed:
```bash
$ cat VERSION
10.5.6-rc44
./scripts/release_candidate.sh
```
## QA OTA channel
## QA
Publish an OTA to the `qa` channel can be done by pushing a new branch
to github with `qa/` prefix.
@ -73,27 +18,17 @@ git checkout -b qa/<some-name>
git push origin qa/<some-name>
```
or to build a QA image from an existing branch:
```bash
git checkout -b some-feature
git commit -am "build out some feature"
git push origin some-feature some-feature:qa/some-featuer
```
## Stable OTA channel
Publish an OTA to the `stable` OTA channel can be
done by pushing anything to the master branch:
## Production
Publish an OTA to the `stable` channel can be done by:
```bash
git checkout -b rel-<version>
# update VERSION
echo $NEW_VERSION > VERSION
# update CHANGELOG.md
$EDITOR CHANGELOG.md
git checkout -b rel-$(cat VERSION)
git commit -am 'Release v$(cat VERSION)'
git push origin rel-$(cat VERSION)
# update README.md
git commit -am "Release v<version>"
git push origin rel-<version>
# open pull request
# merge pull request
# publish release once CI has completed

View File

@ -1,4 +1,3 @@
[
line_length: 80,
inputs: ["*.{ex,exs}", "{config,priv,lib,test}/**/*.{ex,exs}"]
]

View File

@ -25,4 +25,3 @@ farmbot_ng-*.tar
*.sqlite3
*.so
*.hex
*.coverdata

View File

@ -1,4 +1,9 @@
use Mix.Config
config :farmbot_celery_script, FarmbotCeleryScript.SysCalls,
sys_calls: FarmbotCeleryScript.SysCalls.Stubs
if Mix.env() == :test do
config :farmbot_celery_script, FarmbotCeleryScript.SysCalls,
sys_calls: Farmbot.TestSupport.CeleryScript.TestSysCalls
else
config :farmbot_celery_script, FarmbotCeleryScript.SysCalls,
sys_calls: FarmbotCeleryScript.SysCalls.Stubs
end

View File

@ -1,7 +1,5 @@
{
"skip_files": [
"lib/farmbot_celery_script/compiler/tools.ex"
],
"minimum_coverage": 53,
"treat_no_relevant_lines_as_covered": true
]
}

View File

@ -22,14 +22,13 @@
{
"name": "ALLOWED_MESSAGE_TYPES",
"allowed_values": [
"assertion",
"busy",
"debug",
"error",
"fun",
"info",
"success",
"warn"
"busy",
"warn",
"error",
"info",
"fun",
"debug"
]
},
{
@ -56,15 +55,6 @@
1
]
},
{
"name": "ALLOWED_ASSERTION_TYPES",
"allowed_values": [
"abort",
"recover",
"abort_recover",
"continue"
]
},
{
"name": "AllowedPinTypes",
"allowed_values": [
@ -99,10 +89,10 @@
"name": "LegalSequenceKind",
"allowed_values": [
"_if",
"assertion",
"calibrate",
"change_ownership",
"check_updates",
"dump_info",
"emergency_lock",
"emergency_unlock",
"execute",
@ -120,6 +110,7 @@
"read_status",
"reboot",
"remove_farmware",
"resource_update",
"send_message",
"set_servo_angle",
"set_user_env",
@ -127,7 +118,6 @@
"take_photo",
"toggle_pin",
"update_farmware",
"update_resource",
"wait",
"write_pin",
"zero"
@ -217,8 +207,7 @@
"planned",
"planted",
"harvested",
"sprouted",
"removed"
"sprouted"
]
},
{
@ -226,18 +215,24 @@
"allowed_values": [
"GenericPointer",
"ToolSlot",
"Plant",
"Weed"
"Plant"
]
},
{
"name": "resource_type",
"allowed_values": [
"Device",
"Point",
"FarmEvent",
"Image",
"Log",
"Peripheral",
"Plant",
"Point",
"Regimen",
"Sequence",
"Tool",
"ToolSlot",
"Weed",
"User",
"GenericPointer"
]
},
@ -267,13 +262,11 @@
"z",
"pin_type",
"pointer_id",
"point_group_id",
"pointer_type",
"pin_mode",
"sequence_id",
"lhs",
"op",
"priority",
"channel_name",
"message_type",
"tool_id",
@ -281,22 +274,19 @@
"axis",
"message",
"speed",
"resource_type",
"assertion_type",
"lua",
"resource"
"resource_type"
]
},
{
"name": "LegalKindString",
"allowed_values": [
"Assertion",
"If",
"Calibrate",
"ChangeOwnership",
"Channel",
"CheckUpdates",
"Coordinate",
"DumpInfo",
"EmergencyLock",
"EmergencyUnlock",
"ExecuteScript",
@ -342,10 +332,7 @@
"MoveAbsolute",
"WritePin",
"ReadPin",
"ResourceUpdate",
"Resource",
"UpdateResource",
"PointGroup"
"ResourceUpdate"
]
}
],
@ -408,10 +395,6 @@
{
"tag": "identifier",
"name": "identifier"
},
{
"tag": "point_group",
"name": "point_group"
}
]
},
@ -643,15 +626,6 @@
}
]
},
{
"name": "point_group_id",
"allowed_values": [
{
"tag": "integer",
"name": "Integer"
}
]
},
{
"name": "pointer_type",
"allowed_values": [
@ -701,15 +675,6 @@
}
]
},
{
"name": "priority",
"allowed_values": [
{
"tag": "integer",
"name": "Integer"
}
]
},
{
"name": "channel_name",
"allowed_values": [
@ -781,53 +746,9 @@
"name": "resource_type"
}
]
},
{
"name": "assertion_type",
"allowed_values": [
{
"tag": "ALLOWED_ASSERTION_TYPES",
"name": "ALLOWED_ASSERTION_TYPES"
}
]
},
{
"name": "lua",
"allowed_values": [
{
"tag": "string",
"name": "String"
}
]
},
{
"name": "resource",
"allowed_values": [
{
"tag": "identifier",
"name": "identifier"
},
{
"tag": "resource",
"name": "resource"
}
]
}
],
"nodes": [
{
"allowed_args": [
"assertion_type",
"_then",
"lua"
],
"allowed_body_types": [],
"name": "assertion",
"tags": [
"*"
],
"docs": ""
},
{
"allowed_args": [
"lhs",
@ -911,6 +832,18 @@
],
"docs": ""
},
{
"allowed_args": [],
"allowed_body_types": [],
"name": "dump_info",
"tags": [
"function",
"network_user",
"disk_user",
"api_writer"
],
"docs": "Sends an info dump to server administrators for troubleshooting."
},
{
"allowed_args": [],
"allowed_body_types": [],
@ -1231,14 +1164,13 @@
},
{
"allowed_args": [
"label",
"priority"
"label"
],
"allowed_body_types": [
"assertion",
"calibrate",
"change_ownership",
"check_updates",
"dump_info",
"emergency_lock",
"emergency_unlock",
"execute",
@ -1257,7 +1189,7 @@
"read_status",
"reboot",
"remove_farmware",
"update_resource",
"resource_update",
"send_message",
"set_servo_angle",
"set_user_env",
@ -1307,10 +1239,10 @@
"locals"
],
"allowed_body_types": [
"assertion",
"calibrate",
"change_ownership",
"check_updates",
"dump_info",
"emergency_lock",
"emergency_unlock",
"execute",
@ -1329,7 +1261,7 @@
"read_status",
"reboot",
"remove_farmware",
"update_resource",
"resource_update",
"send_message",
"set_servo_angle",
"set_user_env",
@ -1542,45 +1474,6 @@
"network_user"
],
"docs": ""
},
{
"allowed_args": [
"resource_type",
"resource_id"
],
"allowed_body_types": [],
"name": "resource",
"tags": [
"network_user"
],
"docs": ""
},
{
"allowed_args": [
"resource"
],
"allowed_body_types": [
"pair"
],
"name": "update_resource",
"tags": [
"function",
"api_writer",
"network_user"
],
"docs": ""
},
{
"allowed_args": [
"point_group_id"
],
"allowed_body_types": [],
"name": "point_group",
"tags": [
"data",
"list_like"
],
"docs": ""
}
]
}
}

View File

@ -77,8 +77,7 @@ defmodule FarmbotCeleryScript.AST do
end
@spec new(atom, map, [map]) :: t()
def new(kind, args, body, comment \\ nil, meta \\ nil)
when is_map(args) and is_list(body) do
def new(kind, args, body, comment \\ nil, meta \\ nil) when is_map(args) and is_list(body) do
%AST{
kind: String.to_atom(to_string(kind)),
args: args,

View File

@ -5,35 +5,10 @@ defmodule FarmbotCeleryScript.AST.Factory do
alias FarmbotCeleryScript.AST
@doc """
Create an empty AST WITH ARG SET TO `nil`.
iex> new()
%FarmbotCeleryScript.AST{
args: nil,
body: [],
comment: nil,
kind: nil,
meta: nil
}
"""
def new do
%AST{body: []}
end
@doc """
Create a new AST to work with. Strings `kind`s are
converted to symbols.
iex> new("foo")
%FarmbotCeleryScript.AST{
args: %{},
body: [],
comment: nil,
kind: :foo,
meta: nil
}
"""
def new(kind, args \\ %{}, body \\ []) do
AST.new(kind, Map.new(args), body)
end
@ -42,155 +17,72 @@ defmodule FarmbotCeleryScript.AST.Factory do
%AST{ast | kind: :rpc_request, args: %{label: label}, body: []}
end
def execute(%AST{} = ast, sequence_id) do
ast
|> add_body_node(new(:execute, %{sequence_id: sequence_id}))
end
def read_pin(%AST{} = ast, pin_number, pin_mode) do
ast
|> add_body_node(
new(:read_pin, %{pin_number: pin_number, pin_mode: pin_mode})
)
|> add_body_node(new(:read_pin, %{pin_number: pin_number, pin_mode: pin_mode}))
end
@doc """
iex> (new() |> rpc_request("x") |> set_pin_io_mode(13, 1)).body
[%FarmbotCeleryScript.AST{
kind: :set_pin_io_mode,
args: %{ pin_io_mode: 1, pin_number: 13 },
body: [],
comment: nil,
meta: nil
}]
"""
def set_pin_io_mode(%AST{} = ast, pin_number, pin_io_mode) do
args = %{pin_number: pin_number, pin_io_mode: pin_io_mode}
ast |> add_body_node(new(:set_pin_io_mode, args))
ast
|> add_body_node(new(:set_pin_io_mode, %{pin_number: pin_number, pin_io_mode: pin_io_mode}))
end
def dump_info(%AST{} = ast) do
ast
|> add_body_node(new(:dump_info))
end
@doc """
iex> (new() |> rpc_request("x") |> emergency_lock()).body
[%FarmbotCeleryScript.AST{
body: [],
comment: nil,
meta: nil,
args: %{},
kind: :emergency_lock
}]
"""
def emergency_lock(%AST{} = ast) do
ast |> add_body_node(new(:emergency_lock))
ast
|> add_body_node(new(:emergency_lock))
end
@doc """
iex> (new() |> rpc_request("x") |> emergency_unlock()).body
[%FarmbotCeleryScript.AST{
body: [],
comment: nil,
meta: nil,
args: %{},
kind: :emergency_unlock
}]
"""
def emergency_unlock(%AST{} = ast) do
ast |> add_body_node(new(:emergency_unlock))
ast
|> add_body_node(new(:emergency_unlock))
end
@doc """
iex> (new() |> rpc_request("x") |> read_status()).body
[%FarmbotCeleryScript.AST{
body: [],
comment: nil,
meta: nil,
args: %{},
kind: :read_status
}]
"""
def read_status(%AST{} = ast) do
ast |> add_body_node(new(:read_status))
ast
|> add_body_node(new(:read_status))
end
@doc """
iex> (new() |> rpc_request("x") |> power_off()).body
[%FarmbotCeleryScript.AST{
body: [],
comment: nil,
meta: nil,
args: %{},
kind: :power_off
}]
"""
def power_off(%AST{} = ast) do
ast |> add_body_node(new(:power_off))
ast
|> add_body_node(new(:power_off))
end
@doc """
iex> (new() |> rpc_request("x") |> reboot()).body
[%FarmbotCeleryScript.AST{
body: [],
comment: nil,
meta: nil,
args: %{},
kind: :reboot
}]
"""
def reboot(%AST{} = ast) do
ast |> add_body_node(new(:reboot))
ast
|> add_body_node(new(:reboot))
end
@doc """
iex> (new() |> rpc_request("x") |> sync()).body
[%FarmbotCeleryScript.AST{
body: [],
comment: nil,
meta: nil,
args: %{},
kind: :sync
}]
"""
def sync(%AST{} = ast) do
ast |> add_body_node(new(:sync))
ast
|> add_body_node(new(:sync))
end
@doc """
iex> (new() |> rpc_request("x") |> take_photo()).body
[%FarmbotCeleryScript.AST{
body: [],
comment: nil,
meta: nil,
args: %{},
kind: :take_photo
}]
"""
def take_photo(%AST{} = ast) do
ast |> add_body_node(new(:take_photo))
ast
|> add_body_node(new(:take_photo))
end
@doc """
iex> (new() |> rpc_request("x") |> flash_firmware("arduino")).body
[%FarmbotCeleryScript.AST{
kind: :flash_firmware,
comment: nil,
meta: nil,
args: %{package: "arduino"},
body: [],
}]
"""
def flash_firmware(%AST{} = ast, package) when is_binary(package) do
ast |> add_body_node(new(:flash_firmware, %{package: package}))
end
@doc """
iex> (new() |> rpc_request("x") |> factory_reset("arduino")).body
[%FarmbotCeleryScript.AST{
kind: :factory_reset,
comment: nil,
meta: nil,
args: %{package: "arduino"},
body: [],
}]
"""
def factory_reset(%AST{} = ast, package) do
ast |> add_body_node(new(:factory_reset, %{package: package}))
ast
|> add_body_node(new(:flash_firmware, %{package: package}))
end
def add_body_node(%AST{body: body} = ast, %AST{} = body_node) do
%{ast | body: body ++ [body_node]}
end
def factory_reset(%AST{} = ast, package) do
ast
|> add_body_node(new(:factory_reset, %{package: package}))
end
end

View File

@ -11,10 +11,17 @@ defmodule FarmbotCeleryScript.Compiler do
Compiler.IdentifierSanitizer
}
@doc "Sets debug mode for the compiler"
def debug_mode(bool \\ true) do
old = Application.get_env(:farmbot_celery_script, __MODULE__, [])
new = Keyword.put(old, :debug, bool)
Application.put_env(:farmbot_celery_script, __MODULE__, new)
bool
end
@doc "Returns current debug mode value"
def debug_mode?() do
# Set this to `true` when debuging.
false
Application.get_env(:farmbot_celery_script, __MODULE__)[:debug] || false
end
@valid_entry_points [:sequence, :rpc_request]
@ -87,28 +94,27 @@ defmodule FarmbotCeleryScript.Compiler do
defdelegate assertion(ast, env), to: Compiler.Assertion
defdelegate calibrate(ast, env), to: Compiler.AxisControl
defdelegate coordinate(ast, env), to: Compiler.DataControl
defdelegate execute_script(ast, env), to: Compiler.Farmware
defdelegate execute(ast, env), to: Compiler.Execute
defdelegate execute_script(ast, env), to: Compiler.Farmware
defdelegate find_home(ast, env), to: Compiler.AxisControl
defdelegate home(ast, env), to: Compiler.AxisControl
defdelegate unquote(:_if)(ast, env), to: Compiler.If
defdelegate install_first_party_farmware(ast, env), to: Compiler.Farmware
defdelegate move_absolute(ast, env), to: Compiler.AxisControl
defdelegate move_relative(ast, env), to: Compiler.AxisControl
defdelegate named_pin(ast, env), to: Compiler.DataControl
defdelegate point(ast, env), to: Compiler.DataControl
defdelegate read_pin(ast, env), to: Compiler.PinControl
defdelegate resource(ast, env), to: Compiler.DataControl
defdelegate resource_update(ast, env), to: Compiler.DataControl
defdelegate rpc_request(ast, env), to: Compiler.RPCRequest
defdelegate sequence(ast, env), to: Compiler.Sequence
defdelegate set_pin_io_mode(ast, env), to: Compiler.PinControl
defdelegate set_servo_angle(ast, env), to: Compiler.PinControl
defdelegate set_user_env(ast, env), to: Compiler.Farmware
defdelegate take_photo(ast, env), to: Compiler.Farmware
defdelegate toggle_pin(ast, env), to: Compiler.PinControl
defdelegate tool(ast, env), to: Compiler.DataControl
defdelegate unquote(:_if)(ast, env), to: Compiler.If
defdelegate toggle_pin(ast, env), to: Compiler.PinControl
defdelegate update_farmware(ast, env), to: Compiler.Farmware
defdelegate update_resource(ast, env), to: Compiler.UpdateResource
defdelegate variable_declaration(ast, env), to: Compiler.VariableDeclaration
defdelegate write_pin(ast, env), to: Compiler.PinControl
defdelegate zero(ast, env), to: Compiler.AxisControl
@ -149,20 +155,14 @@ defmodule FarmbotCeleryScript.Compiler do
end
end
def send_message(
%{args: %{message: msg, message_type: type}, body: channels},
env
) do
def send_message(%{args: %{message: msg, message_type: type}, body: channels}, env) do
# body gets turned into a list of atoms.
# Example:
# [{kind: "channel", args: {channel_name: "email"}}]
# is turned into:
# [:email]
channels =
Enum.map(channels, fn %{
kind: :channel,
args: %{channel_name: channel_name}
} ->
Enum.map(channels, fn %{kind: :channel, args: %{channel_name: channel_name}} ->
String.to_atom(channel_name)
end)
@ -218,9 +218,7 @@ defmodule FarmbotCeleryScript.Compiler do
def flash_firmware(%{args: %{package: package_name}}, env) do
quote location: :keep do
FarmbotCeleryScript.SysCalls.flash_firmware(
unquote(compile_ast(package_name, env))
)
FarmbotCeleryScript.SysCalls.flash_firmware(unquote(compile_ast(package_name, env)))
end
end
@ -244,9 +242,7 @@ defmodule FarmbotCeleryScript.Compiler do
def factory_reset(%{args: %{package: package}}, env) do
quote location: :keep do
FarmbotCeleryScript.SysCalls.factory_reset(
unquote(compile_ast(package, env))
)
FarmbotCeleryScript.SysCalls.factory_reset(unquote(compile_ast(package, env)))
end
end
@ -273,14 +269,20 @@ defmodule FarmbotCeleryScript.Compiler do
end
end
def dump_info(_, _env) do
quote location: :keep do
FarmbotCeleryScript.SysCalls.dump_info()
end
end
defp print_compiled_code(compiled) do
IO.puts("=== START ===")
IO.puts("========")
compiled
|> Macro.to_string()
|> Code.format_string!()
|> IO.puts()
IO.puts("=== END ===\n\n")
IO.puts("========\n\n")
end
end

View File

@ -2,7 +2,7 @@ defmodule FarmbotCeleryScript.Compiler.IdentifierSanitizer do
@moduledoc """
Responsible for ensuring variable names in Sequences are clean.
This is done because identifiers are `unquote`d and the user controls
the data inside them. To prevent things like
the data inside them. To prevent things like
`"System.cmd("rm -rf /*/**")"` being evaluated, all identifiers
are sanitized by prepending a token and hashing the value.
"""

View File

@ -80,8 +80,7 @@ defmodule FarmbotCeleryScript.Compiler.Utils do
var =
quote location: :keep do
{unquote(next_scope_var_name),
unquote(Compiler.compile_ast(data_value, env))}
{unquote(next_scope_var_name), unquote(Compiler.compile_ast(data_value, env))}
end
compile_params_to_function_args(rest, env, [var | acc])
@ -118,19 +117,12 @@ defmodule FarmbotCeleryScript.Compiler.Utils do
parent = Keyword.get(params, :parent, %{x: 100, y: 200, z: 300})
"""
def compile_param_declaration(
%{args: %{label: var_name, default_value: default}},
env
) do
def compile_param_declaration(%{args: %{label: var_name, default_value: default}}, env) do
var_name = IdentifierSanitizer.to_variable(var_name)
quote location: :keep do
unquote({var_name, env, __MODULE__}) =
Keyword.get(
params,
unquote(var_name),
unquote(Compiler.compile_ast(default, env))
)
Keyword.get(params, unquote(var_name), unquote(Compiler.compile_ast(default, env)))
end
end
@ -160,15 +152,11 @@ defmodule FarmbotCeleryScript.Compiler.Utils do
]
}
"""
def compile_param_application(
%{args: %{label: var_name, data_value: value}},
env
) do
def compile_param_application(%{args: %{label: var_name, data_value: value}}, env) do
var_name = IdentifierSanitizer.to_variable(var_name)
quote location: :keep do
unquote({var_name, [], __MODULE__}) =
unquote(Compiler.compile_ast(value, env))
unquote({var_name, [], __MODULE__}) = unquote(Compiler.compile_ast(value, env))
end
end
@ -180,16 +168,13 @@ defmodule FarmbotCeleryScript.Compiler.Utils do
end)
end
def add_sequence_init_and_complete_logs(steps, sequence_name)
when is_binary(sequence_name) do
def add_sequence_init_and_complete_logs(steps, sequence_name) when is_binary(sequence_name) do
# This looks really weird because of the logs before and
# after the compiled steps
List.flatten([
quote do
fn ->
FarmbotCeleryScript.SysCalls.sequence_init_log(
"Starting #{unquote(sequence_name)}"
)
FarmbotCeleryScript.SysCalls.sequence_init_log("Starting #{unquote(sequence_name)}")
end
end,
steps,
@ -216,9 +201,7 @@ defmodule FarmbotCeleryScript.Compiler.Utils do
fn _ ->
[
fn ->
FarmbotCeleryScript.SysCalls.sequence_init_log(
"Starting #{unquote(sequence_name)}"
)
FarmbotCeleryScript.SysCalls.sequence_init_log("Starting #{unquote(sequence_name)}")
end
]
end

View File

@ -4,11 +4,7 @@ defmodule FarmbotCeleryScript.Compiler.Assertion do
@doc "`Assert` is a internal node useful for self testing."
def assertion(
%{
args: %{
lua: expression,
assertion_type: assertion_type,
_then: then_ast
},
args: %{lua: expression, assertion_type: assertion_type, _then: then_ast},
comment: comment
},
env
@ -84,10 +80,7 @@ defmodule FarmbotCeleryScript.Compiler.Assertion do
then_block ++
[
FarmbotCeleryScript.Compiler.compile(
%AST{kind: :abort, args: %{}},
[]
)
FarmbotCeleryScript.Compiler.compile(%AST{kind: :abort, args: %{}}, [])
]
end
end

View File

@ -1,17 +1,12 @@
defmodule FarmbotCeleryScript.Compiler.AxisControl do
alias FarmbotCeleryScript.Compiler
# Compiles move_absolute
def move_absolute(
%{args: %{location: location, offset: offset, speed: speed}},
env
) do
def move_absolute(%{args: %{location: location, offset: offset, speed: speed}}, env) do
quote location: :keep do
# Extract the location arg
with %{x: locx, y: locy, z: locz} =
unquote(Compiler.compile_ast(location, env)),
with %{x: locx, y: locy, z: locz} = unquote(Compiler.compile_ast(location, env)),
# Extract the offset arg
%{x: offx, y: offy, z: offz} =
unquote(Compiler.compile_ast(offset, env)) do
%{x: offx, y: offy, z: offz} = unquote(Compiler.compile_ast(offset, env)) do
# Subtract the location from offset.
# Note: list syntax here for readability.
[x, y, z] = [
@ -23,11 +18,7 @@ defmodule FarmbotCeleryScript.Compiler.AxisControl do
x_str = FarmbotCeleryScript.FormatUtil.format_float(x)
y_str = FarmbotCeleryScript.FormatUtil.format_float(y)
z_str = FarmbotCeleryScript.FormatUtil.format_float(z)
FarmbotCeleryScript.SysCalls.log(
"Moving to (#{x_str}, #{y_str}, #{z_str})",
true
)
FarmbotCeleryScript.SysCalls.log("Moving to (#{x_str}, #{y_str}, #{z_str})", true)
FarmbotCeleryScript.SysCalls.move_absolute(
x,
@ -45,12 +36,9 @@ defmodule FarmbotCeleryScript.Compiler.AxisControl do
with locx when is_number(locx) <- unquote(Compiler.compile_ast(x, env)),
locy when is_number(locy) <- unquote(Compiler.compile_ast(y, env)),
locz when is_number(locz) <- unquote(Compiler.compile_ast(z, env)),
curx when is_number(curx) <-
FarmbotCeleryScript.SysCalls.get_current_x(),
cury when is_number(cury) <-
FarmbotCeleryScript.SysCalls.get_current_y(),
curz when is_number(curz) <-
FarmbotCeleryScript.SysCalls.get_current_z() do
curx when is_number(curx) <- FarmbotCeleryScript.SysCalls.get_current_x(),
cury when is_number(cury) <- FarmbotCeleryScript.SysCalls.get_current_y(),
curz when is_number(curz) <- FarmbotCeleryScript.SysCalls.get_current_z() do
# Combine them
x = locx + curx
y = locy + cury
@ -89,13 +77,8 @@ defmodule FarmbotCeleryScript.Compiler.AxisControl do
# compiles find_home
def find_home(%{args: %{axis: axis}}, env) do
quote location: :keep do
with axis when axis in ["x", "y", "z"] <-
unquote(Compiler.compile_ast(axis, env)) do
FarmbotCeleryScript.SysCalls.log(
"Finding home on the #{String.upcase(axis)} axis",
true
)
with axis when axis in ["x", "y", "z"] <- unquote(Compiler.compile_ast(axis, env)) do
FarmbotCeleryScript.SysCalls.log("Finding home on the #{String.upcase(axis)} axis", true)
FarmbotCeleryScript.SysCalls.find_home(axis)
else
{:error, reason} ->
@ -109,8 +92,7 @@ defmodule FarmbotCeleryScript.Compiler.AxisControl do
quote location: :keep do
FarmbotCeleryScript.SysCalls.log("Going to home on all axes", true)
with speed when is_number(speed) <-
unquote(Compiler.compile_ast(speed, env)),
with speed when is_number(speed) <- unquote(Compiler.compile_ast(speed, env)),
:ok <- FarmbotCeleryScript.SysCalls.home("z", speed),
:ok <- FarmbotCeleryScript.SysCalls.home("y", speed) do
FarmbotCeleryScript.SysCalls.home("x", speed)
@ -121,15 +103,9 @@ defmodule FarmbotCeleryScript.Compiler.AxisControl do
# compiles home
def home(%{args: %{axis: axis, speed: speed}}, env) do
quote location: :keep do
with axis when axis in ["x", "y", "z"] <-
unquote(Compiler.compile_ast(axis, env)),
speed when is_number(speed) <-
unquote(Compiler.compile_ast(speed, env)) do
FarmbotCeleryScript.SysCalls.log(
"Going to home on the #{String.upcase(axis)} axis",
true
)
with axis when axis in ["x", "y", "z"] <- unquote(Compiler.compile_ast(axis, env)),
speed when is_number(speed) <- unquote(Compiler.compile_ast(speed, env)) do
FarmbotCeleryScript.SysCalls.log("Going to home on the #{String.upcase(axis)} axis", true)
FarmbotCeleryScript.SysCalls.home(axis, speed)
else
{:error, reason} ->
@ -153,13 +129,8 @@ defmodule FarmbotCeleryScript.Compiler.AxisControl do
# compiles zero
def zero(%{args: %{axis: axis}}, env) do
quote location: :keep do
with axis when axis in ["x", "y", "z"] <-
unquote(Compiler.compile_ast(axis, env)) do
FarmbotCeleryScript.SysCalls.log(
"Zeroing the #{String.upcase(axis)} axis",
true
)
with axis when axis in ["x", "y", "z"] <- unquote(Compiler.compile_ast(axis, env)) do
FarmbotCeleryScript.SysCalls.log("Zeroing the #{String.upcase(axis)} axis", true)
FarmbotCeleryScript.SysCalls.zero(axis)
else
{:error, reason} ->
@ -186,13 +157,8 @@ defmodule FarmbotCeleryScript.Compiler.AxisControl do
# compiles calibrate
def calibrate(%{args: %{axis: axis}}, env) do
quote location: :keep do
with axis when axis in ["x", "y", "z"] <-
unquote(Compiler.compile_ast(axis, env)) do
FarmbotCeleryScript.SysCalls.log(
"Calibrating the #{String.upcase(axis)} axis",
true
)
with axis when axis in ["x", "y", "z"] <- unquote(Compiler.compile_ast(axis, env)) do
FarmbotCeleryScript.SysCalls.log("Calibrating the #{String.upcase(axis)} axis", true)
FarmbotCeleryScript.SysCalls.calibrate(axis)
else
{:error, reason} ->

View File

@ -1,19 +1,6 @@
defmodule FarmbotCeleryScript.Compiler.DataControl do
alias FarmbotCeleryScript.Compiler
def resource(ast, _env) do
IO.puts("======")
IO.inspect(ast)
# %FarmbotCeleryScript.AST{
# args: %{resource_id: 0, resource_type: "Device"},
# body: [],
# comment: nil,
# kind: :resource,
# meta: nil
# }
raise "TODO: Pull resource from DB?"
end
# compiles coordinate
# Coordinate should return a vec3
def coordinate(%{args: %{x: x, y: y, z: z}}, env) do
@ -53,4 +40,27 @@ defmodule FarmbotCeleryScript.Compiler.DataControl do
)
end
end
def resource_update(
%{args: %{resource_type: kind, resource_id: id, label: label, value: value}, body: body},
env
) do
initial = %{label => value}
# Technically now body isn't supported by this node.
extra =
Map.new(body, fn %{args: %{label: label, data_value: value}} ->
{label, value}
end)
# Make sure the initial stuff higher most priority
params = Map.merge(extra, initial)
quote do
FarmbotCeleryScript.SysCalls.resource_update(
unquote(Compiler.compile_ast(kind, env)),
unquote(Compiler.compile_ast(id, env)),
unquote(Macro.escape(params))
)
end
end
end

View File

@ -13,8 +13,7 @@ defmodule FarmbotCeleryScript.Compiler.Execute do
loop_parameter_appl_ast =
Enum.find_value(parameter_applications, fn
# check if this parameter_application is a iterable type
%{kind: :parameter_application, args: %{data_value: %{kind: kind}}} =
iterable
%{kind: :parameter_application, args: %{data_value: %{kind: kind}}} = iterable
when kind in @iterables ->
iterable
@ -39,10 +38,7 @@ defmodule FarmbotCeleryScript.Compiler.Execute do
%FarmbotCeleryScript.AST{kind: :sequence} = celery_ast ->
celery_args =
celery_ast.args
|> Map.put(
:sequence_name,
celery_ast.args[:name] || celery_ast.meta[:sequence_name]
)
|> Map.put(:sequence_name, celery_ast.args[:name] || celery_ast.meta[:sequence_name])
|> Map.put(:locals, %{
celery_ast.args.locals
| body: celery_ast.args.locals.body ++ unquote(param_appls)
@ -57,20 +53,13 @@ defmodule FarmbotCeleryScript.Compiler.Execute do
end
end
def compile_execute(
%{args: %{sequence_id: id}, body: parameter_applications},
env
) do
def compile_execute(%{args: %{sequence_id: id}, body: parameter_applications}, env) do
quote location: :keep do
# We have to lookup the sequence by it's id.
case FarmbotCeleryScript.SysCalls.get_sequence(unquote(id)) do
%FarmbotCeleryScript.AST{} = ast ->
# compile the ast
env =
unquote(
compile_params_to_function_args(parameter_applications, env)
)
env = unquote(compile_params_to_function_args(parameter_applications, env))
FarmbotCeleryScript.Compiler.compile(ast, env)
error ->

View File

@ -30,10 +30,7 @@ defmodule FarmbotCeleryScript.Compiler.Farmware do
kvs =
Enum.map(pairs, fn %{kind: :pair, args: %{label: key, value: value}} ->
quote location: :keep do
FarmbotCeleryScript.SysCalls.set_user_env(
unquote(key),
unquote(value)
)
FarmbotCeleryScript.SysCalls.set_user_env(unquote(key), unquote(value))
end
end)

View File

@ -4,15 +4,7 @@ defmodule FarmbotCeleryScript.Compiler.If do
# Compiles an if statement.
def unquote(:_if)(
%{
args: %{
_then: then_ast,
_else: else_ast,
lhs: lhs_ast,
op: op,
rhs: rhs
}
},
%{args: %{_then: then_ast, _else: else_ast, lhs: lhs_ast, op: op, rhs: rhs}},
env
) do
rhs = Compiler.compile_ast(rhs, env)
@ -38,10 +30,7 @@ defmodule FarmbotCeleryScript.Compiler.If do
"pin" <> pin ->
quote [location: :keep],
do:
FarmbotCeleryScript.SysCalls.read_cached_pin(
unquote(String.to_integer(pin))
)
do: FarmbotCeleryScript.SysCalls.read_cached_pin(unquote(String.to_integer(pin)))
# Named pin has two intents here
# in this case we want to read the named pin.

View File

@ -1,10 +1,7 @@
defmodule FarmbotCeleryScript.Compiler.PinControl do
alias FarmbotCeleryScript.Compiler
# compiles write_pin
def write_pin(
%{args: %{pin_number: num, pin_mode: mode, pin_value: value}},
env
) do
def write_pin(%{args: %{pin_number: num, pin_mode: mode, pin_value: value}}, env) do
quote location: :keep do
pin = unquote(Compiler.compile_ast(num, env))
mode = unquote(Compiler.compile_ast(mode, env))
@ -26,10 +23,7 @@ defmodule FarmbotCeleryScript.Compiler.PinControl do
end
# compiles set_servo_angle
def set_servo_angle(
%{args: %{pin_number: pin_number, pin_value: pin_value}},
env
) do
def set_servo_angle(%{args: %{pin_number: pin_number, pin_value: pin_value}}, env) do
quote location: :keep do
pin = unquote(Compiler.compile_ast(pin_number, env))
angle = unquote(Compiler.compile_ast(pin_value, env))
@ -39,10 +33,7 @@ defmodule FarmbotCeleryScript.Compiler.PinControl do
end
# compiles set_pin_io_mode
def set_pin_io_mode(
%{args: %{pin_number: pin_number, pin_io_mode: mode}},
env
) do
def set_pin_io_mode(%{args: %{pin_number: pin_number, pin_io_mode: mode}}, env) do
quote location: :keep do
pin = unquote(Compiler.compile_ast(pin_number, env))
mode = unquote(Compiler.compile_ast(mode, env))

View File

@ -1,18 +1,16 @@
defmodule FarmbotCeleryScript.Compiler.Sequence do
import FarmbotCeleryScript.Compiler.Utils
alias FarmbotCeleryScript.Compiler.IdentifierSanitizer
@iterables [:point_group, :every_point]
def sequence(%{args: %{locals: %{body: params_or_iterables}}} = ast, env) do
# if there is an iterable AST here,
# if there is an iterable AST here,
# we need to compile _many_ sequences, not just one.
iterable_ast =
loop_parameter_appl_ast =
Enum.find_value(params_or_iterables, fn
# check if this parameter_application is a iterable type
%{kind: :parameter_application, args: %{data_value: %{kind: kind}}} =
iterable
%{kind: :parameter_application, args: %{data_value: %{kind: kind}}} = iterable
when kind in @iterables ->
iterable
@ -20,42 +18,48 @@ defmodule FarmbotCeleryScript.Compiler.Sequence do
false
end)
if iterable_ast do
compile_sequence_iterable(iterable_ast, ast, env)
else
compile_sequence(ast, env)
end
if loop_parameter_appl_ast,
do: compile_sequence_iterable(loop_parameter_appl_ast, ast, env),
else: compile_sequence(ast, env)
end
def compile_sequence_iterable(
iterable_ast,
%{
args: %{locals: %{body: _} = locals} = sequence_args,
meta: sequence_meta
} = sequence_ast,
loop_parameter_appl_ast,
%{args: %{locals: %{body: params} = locals} = sequence_args, meta: sequence_meta} =
sequence_ast,
env
) do
sequence_name =
sequence_meta[:sequence_name] || sequence_args[:sequence_name]
sequence_name = sequence_meta[:sequence_name] || sequence_args[:sequence_name]
# remove the iterable from the parameter applications,
# since it will be injected after this.
_params =
Enum.reduce(params, [], fn
# Remove point_group from parameter appls
%{kind: :parameter_application, args: %{data_value: %{kind: :point_group}}}, acc -> acc
# Remove every_point from parameter appls
%{kind: :parameter_application, args: %{data_value: %{kind: :every_point}}}, acc -> acc
# Everything else gets added back
ast, acc -> acc ++ [ast]
end)
# will be a point_group or every_point node
group_ast = iterable_ast.args.data_value
group_ast = loop_parameter_appl_ast.args.data_value
# check if it's a point_group first, then fall back to every_point
point_group_arg =
group_ast.args[:point_group_id] || group_ast.args[:resource_id] ||
group_ast.args[:every_point_type]
# lookup all point_groups related to this value
case FarmbotCeleryScript.SysCalls.find_points_via_group(point_group_arg) do
case FarmbotCeleryScript.SysCalls.get_point_group(point_group_arg) do
{:error, reason} ->
quote location: :keep, do: Macro.escape({:error, unquote(reason)})
%{name: group_name} = point_group ->
total = Enum.count(point_group.point_ids)
# Map over all the points returned by `find_points_via_group/1`
# Map over all the points returned by `get_point_group/1`
{body, _} =
Enum.reduce(point_group.point_ids, {[], 1}, fn point_id,
{acc, index} ->
Enum.reduce(point_group.point_ids, {[], 1}, fn point_id, {acc, index} ->
# check if it's an every_point node first, if not fall back go generic pointer
pointer_type = group_ast.args[:every_point_type] || "GenericPointer"
@ -63,7 +67,7 @@ defmodule FarmbotCeleryScript.Compiler.Sequence do
kind: :parameter_application,
args: %{
# inject the replacement with the same label
label: iterable_ast.args.label,
label: loop_parameter_appl_ast.args.label,
data_value: %FarmbotCeleryScript.AST{
kind: :point,
args: %{pointer_type: pointer_type, pointer_id: point_id}
@ -79,10 +83,7 @@ defmodule FarmbotCeleryScript.Compiler.Sequence do
%{name: name, x: x, y: y, z: z} ->
pos = FarmbotCeleryScript.FormatUtil.format_coord(x, y, z)
"unnamed iterable sequence [#{index} / #{total}] - #{name} #{
pos
}"
"unnamed iterable sequence [#{index} / #{total}] - #{name} #{pos}"
_ ->
"unknown iterable [#{index} / #{total}]"
@ -90,7 +91,7 @@ defmodule FarmbotCeleryScript.Compiler.Sequence do
# compile a `sequence` ast, injecting the appropriate `point` ast with
# the matching `label`
# TODO(Connor) - the body of this ast should have the
# TODO(Connor) - the body of this ast should have the
# params as sorted earlier. Figure out why this doesn't work
body =
compile_sequence(
@ -112,48 +113,11 @@ defmodule FarmbotCeleryScript.Compiler.Sequence do
end
end
def create_better_params(body, env) do
parameter_declarations =
Enum.reduce(env, %{}, fn
{key, value}, map ->
encoded_label = "#{key}"
if String.starts_with?(encoded_label, "unsafe_") do
Map.put(map, IdentifierSanitizer.to_string(encoded_label), value)
else
map
end
end)
Enum.reduce(body, parameter_declarations, fn ast, map ->
case ast do
%{kind: :parameter_application} ->
args = Map.fetch!(ast, :args)
label = Map.fetch!(args, :label)
Map.put(map, label, Map.fetch!(args, :data_value))
%{kind: :variable_declaration} ->
args = Map.fetch!(ast, :args)
label = Map.fetch!(args, :label)
Map.put(map, label, Map.fetch!(args, :data_value))
%{kind: :parameter_declaration} ->
map
end
end)
end
def compile_sequence(
%{args: %{locals: %{body: params}} = args, body: block, meta: meta},
env
) do
def compile_sequence(%{args: %{locals: %{body: params}} = args, body: block, meta: meta}, env) do
# Sort the args.body into two arrays.
# The `params` side gets turned into
# a keyword list. These `params` are passed in from a previous sequence.
# The `body` side declares variables in _this_ scope.
# === DON'T USE THIS IN NEW CODE.
# SCHEDULED FOR DEPRECATION.
# USE `better_params` INSTEAD.
{params_fetch, body} =
Enum.reduce(params, {[], []}, fn ast, {params, body} = _acc ->
case ast do
@ -177,8 +141,6 @@ defmodule FarmbotCeleryScript.Compiler.Sequence do
steps = add_sequence_init_and_complete_logs(steps, sequence_name)
better_params = create_better_params(params, env)
[
quote location: :keep do
fn params ->
@ -189,7 +151,7 @@ defmodule FarmbotCeleryScript.Compiler.Sequence do
# parent = Keyword.fetch!(params, :parent)
unquote_splicing(params_fetch)
unquote_splicing(assignments)
better_params = unquote(better_params)
# Unquote the remaining sequence steps.
unquote(steps)
end

View File

@ -1,61 +0,0 @@
defmodule FarmbotCeleryScript.Compiler.UpdateResource do
alias FarmbotCeleryScript.{AST, DotProps}
def update_resource(%AST{args: args, body: body}, _env) do
quote location: :keep do
me = unquote(__MODULE__)
variable = unquote(Map.fetch!(args, :resource))
update = unquote(unpair(body, %{}))
# Go easy on the API...
case variable do
%AST{kind: :identifier} ->
args = Map.fetch!(variable, :args)
label = Map.fetch!(args, :label)
resource = Map.fetch!(better_params, label)
me.do_update(resource, update)
%AST{kind: :point} ->
me.do_update(variable.args, update)
%AST{kind: :resource} ->
me.do_update(variable.args, update)
res ->
raise "Resource error. Please notfiy support: #{inspect(res)}"
end
end
end
def do_update(%{pointer_id: id, pointer_type: kind}, update_params) do
FarmbotCeleryScript.SysCalls.update_resource(kind, id, update_params)
end
def do_update(%{resource_id: id, resource_type: kind}, update_params) do
FarmbotCeleryScript.SysCalls.update_resource(kind, id, update_params)
end
def do_update(%{args: %{pointer_id: id, pointer_type: k}}, update_params) do
FarmbotCeleryScript.SysCalls.update_resource(k, id, update_params)
end
def do_update(other, update) do
raise String.trim("""
MARK AS can only be used to mark resources like plants and devices.
It cannot be used on things like coordinates.
Ensure that your sequences and farm events us MARK AS on plants and not
coordinates (#{inspect(other)} / #{inspect(update)})
""")
end
defp unpair([pair | rest], acc) do
key = Map.fetch!(pair.args, :label)
val = Map.fetch!(pair.args, :value)
next_acc = Map.merge(acc, DotProps.create(key, val))
unpair(rest, next_acc)
end
defp unpair([], acc) do
acc
end
end

View File

@ -2,10 +2,7 @@ defmodule FarmbotCeleryScript.Compiler.VariableDeclaration do
alias FarmbotCeleryScript.{Compiler, Compiler.IdentifierSanitizer}
@doc "Compiles a variable asignment"
def variable_declaration(
%{args: %{label: var_name, data_value: data_value_ast}},
env
) do
def variable_declaration(%{args: %{label: var_name, data_value: data_value_ast}}, env) do
# Compiles the `data_value`
# and assigns the result to a variable named `label`
# Example:
@ -29,8 +26,7 @@ defmodule FarmbotCeleryScript.Compiler.VariableDeclaration do
var_name = IdentifierSanitizer.to_variable(var_name)
quote location: :keep do
unquote({var_name, [], nil}) =
unquote(Compiler.compile_ast(data_value_ast, env))
unquote({var_name, [], nil}) = unquote(Compiler.compile_ast(data_value_ast, env))
end
end
end

View File

@ -18,18 +18,14 @@ defmodule FarmbotCeleryScript.Corpus do
@corpus_tag tag
# Load and decode each arg in the json into an Arg struct
@args Enum.map(args, fn %{"name" => name, "allowed_values" => allowed_values} =
a ->
@args Enum.map(args, fn %{"name" => name, "allowed_values" => allowed_values} = a ->
%Arg{name: name, allowed_values: allowed_values, doc: a["doc"]}
end)
# Load and decode each node in the json into a Node struct.
# This also expands the `allowed_args` into their respective Arg relationship.
@nodes Enum.map(@nodes, fn %{
"name" => name,
"allowed_args" => aa,
"allowed_body_types" => abt
} = n ->
@nodes Enum.map(@nodes, fn %{"name" => name, "allowed_args" => aa, "allowed_body_types" => abt} =
n ->
allowed_args =
Enum.map(aa, fn arg_name ->
Enum.find(@args, fn
@ -38,12 +34,7 @@ defmodule FarmbotCeleryScript.Corpus do
end) || Mix.raise("Unknown CeleryScript argument: #{arg_name}")
end)
%Node{
name: name,
allowed_args: allowed_args,
allowed_body_types: abt,
doc: n["doc"]
}
%Node{name: name, allowed_args: allowed_args, allowed_body_types: abt, doc: n["doc"]}
end)
# Struct should never be created manually.

View File

@ -1,22 +0,0 @@
defmodule FarmbotCeleryScript.DotProps do
@dot "."
@doc ~S"""
Takes a "dotted" key and val.
Returns deeply nested hash.
## Examples
iex> create("foo.bar.baz", 321)
%{"foo" => %{"bar" => %{"baz" => 321}}}
iex> create("foo", "bar")
%{"foo" => "bar"}
"""
def create(dotted, val) do
[key | list] = dotted |> String.split(@dot) |> Enum.reverse()
Enum.reduce(list, %{key => val}, fn next_key, acc ->
%{next_key => acc}
end)
end
end

View File

@ -1,6 +1,4 @@
defmodule FarmbotCeleryScript.FormatUtil do
def format_float(nil), do: nil
def format_float(value) when is_integer(value) do
format_float(value / 1)
end

View File

@ -67,12 +67,7 @@ defmodule FarmbotCeleryScript.Scheduler do
Calls are executed in a first in first out buffer, with things being added
by `execute/2` taking priority.
"""
@spec schedule(
GenServer.server(),
AST.t() | [Compiler.compiled()],
DateTime.t(),
map()
) ::
@spec schedule(GenServer.server(), AST.t() | [Compiler.compiled()], DateTime.t(), map()) ::
{:ok, reference()}
def schedule(scheduler_pid \\ __MODULE__, celery_script, at, data)
@ -152,12 +147,12 @@ defmodule FarmbotCeleryScript.Scheduler do
case DateTime.diff(DateTime.utc_now(), at, :millisecond) do
# now is before the next date
diff_ms when diff_ms < 0 ->
# from_now =
# DateTime.utc_now()
# |> DateTime.add(abs(diff_ms), :millisecond)
# |> Timex.from_now()
# msg = "Next execution is still #{diff_ms}ms too early (#{from_now})"
# Logger.info(msg)
from_now =
DateTime.utc_now()
|> DateTime.add(abs(diff_ms), :millisecond)
|> Timex.from_now()
Logger.info("Next execution is still #{diff_ms}ms too early (#{from_now})")
state
|> schedule_next_checkup(abs(diff_ms))
@ -165,8 +160,8 @@ defmodule FarmbotCeleryScript.Scheduler do
# now is more than the grace period past schedule time
diff_ms when diff_ms > @grace_period_ms ->
# from_now = Timex.from_now(at)
# Logger.info("Next execution is #{diff_ms}ms too late (#{from_now})")
from_now = Timex.from_now(at)
Logger.info("Next execution is #{diff_ms}ms too late (#{from_now})")
state
|> pop_next()
@ -176,9 +171,7 @@ defmodule FarmbotCeleryScript.Scheduler do
# now is late, but less than the grace period late
diff_ms when diff_ms >= 0 when diff_ms <= @grace_period_ms ->
Logger.info(
"Next execution is ready for execution: #{Timex.from_now(at)}"
)
Logger.info("Next execution is ready for execution: #{Timex.from_now(at)}")
state
|> execute_next()
@ -186,15 +179,8 @@ defmodule FarmbotCeleryScript.Scheduler do
end
end
def handle_info(
{:step_complete, {scheduled_at, executed_at, pid}, result},
state
) do
send(
pid,
{FarmbotCeleryScript,
{:scheduled_execution, scheduled_at, executed_at, result}}
)
def handle_info({:step_complete, {scheduled_at, executed_at, pid}, result}, state) do
send(pid, {FarmbotCeleryScript, {:scheduled_execution, scheduled_at, executed_at, result}})
state
|> pop_next()
@ -208,9 +194,7 @@ defmodule FarmbotCeleryScript.Scheduler do
scheduler_pid = self()
scheduled_pid =
spawn(fn ->
StepRunner.step(scheduler_pid, {at, DateTime.utc_now(), pid}, compiled)
end)
spawn(fn -> StepRunner.step(scheduler_pid, {at, DateTime.utc_now(), pid}, compiled) end)
%{state | scheduled_pid: scheduled_pid}
end
@ -307,8 +291,7 @@ defmodule FarmbotCeleryScript.Scheduler do
%{state | monitors: monitors}
end
@spec add(state(), compiled_ast(), DateTime.t(), data :: map(), pid()) ::
state()
@spec add(state(), compiled_ast(), DateTime.t(), data :: map(), pid()) :: state()
defp add(state, compiled, at, data, pid) do
%{state | compiled: [{compiled, at, data, pid} | state.compiled]}
|> index_next()

View File

@ -15,20 +15,18 @@ defmodule FarmbotCeleryScript.SysCalls do
@type error :: {:error, String.t()}
@type ok_or_error :: :ok | error
# "x", "y", or "z"
@type axis :: String.t()
@type package :: String.t()
@type resource_id :: integer()
@callback calibrate(axis) :: ok_or_error
@callback change_ownership(
email :: String.t(),
secret :: binary(),
server :: String.t()
) ::
@callback change_ownership(email :: String.t(), secret :: binary(), server :: String.t()) ::
ok_or_error
@callback check_update() :: ok_or_error
@callback coordinate(x :: number, y :: number, z :: number) ::
%{x: number(), y: number(), z: number()} | error
@callback dump_info() :: ok_or_error
@callback emergency_lock() :: ok_or_error
@callback emergency_unlock() :: ok_or_error
@callback execute_script(package, args :: map()) :: ok_or_error
@ -50,37 +48,26 @@ defmodule FarmbotCeleryScript.SysCalls do
%{x: number(), y: number(), z: number()} | error()
@callback home(axis, speed :: number()) :: ok_or_error
@callback install_first_party_farmware() :: ok_or_error
@callback move_absolute(
x :: number(),
y :: number(),
z :: number(),
speed :: number()
) ::
@callback move_absolute(x :: number(), y :: number(), z :: number(), speed :: number()) ::
ok_or_error
@callback named_pin(named_pin_type :: String.t(), resource_id) ::
map() | integer | error()
# ?
@callback named_pin(named_pin_type :: String.t(), resource_id) :: map() | integer | error()
@callback nothing() :: any()
@callback point(point_type :: String.t(), resource_id) :: number() | error()
@callback power_off() :: ok_or_error
@callback read_pin(pin_num :: number(), pin_mode :: number()) ::
number | error()
@callback read_pin(pin_num :: number(), pin_mode :: number()) :: number | error()
@callback read_cached_pin(pin_num :: number()) :: number | error()
@callback toggle_pin(pin_num :: number()) :: ok_or_error
@callback read_status() :: ok_or_error
@callback reboot() :: ok_or_error
@callback send_message(type :: String.t(), message :: String.t(), [atom]) ::
ok_or_error
@callback resource_update(String.t(), resource_id, map()) :: ok_or_error
@callback send_message(type :: String.t(), message :: String.t(), [atom]) :: ok_or_error
@callback set_servo_angle(pin :: number(), value :: number()) :: ok_or_error
@callback set_pin_io_mode(pin :: number(), mode :: number()) :: ok_or_error
@callback set_user_env(env_name :: String.t(), env_value :: String.t()) ::
ok_or_error
@callback set_user_env(env_name :: String.t(), env_value :: String.t()) :: ok_or_error
@callback sync() :: ok_or_error
@callback wait(millis :: number()) :: ok_or_error
@callback write_pin(
pin_num :: number(),
pin_mode :: number(),
pin_value :: number
) ::
@callback write_pin(pin_num :: number(), pin_mode :: number(), pin_value :: number) ::
ok_or_error
@callback zero(axis) :: ok_or_error
@ -90,14 +77,10 @@ defmodule FarmbotCeleryScript.SysCalls do
@callback eval_assertion(comment :: String.t(), expression :: String.t()) ::
true | false | error()
@callback find_points_via_group(String.t() | resource_id) :: %{
required(:point_ids) => [resource_id]
}
@callback update_resource(kind :: String.t(), resource_id, params :: map()) ::
ok_or_error
@callback get_point_group(String.t() | resource_id) :: %{required(:point_ids) => [resource_id]}
def find_points_via_group(sys_calls \\ @sys_calls, point_group_id) do
point_group_or_error(sys_calls, :find_points_via_group, [point_group_id])
def get_point_group(sys_calls \\ @sys_calls, point_group_id) do
point_group_or_error(sys_calls, :get_point_group, [point_group_id])
end
def format_lhs(sys_calls \\ @sys_calls, lhs)
@ -107,18 +90,14 @@ defmodule FarmbotCeleryScript.SysCalls do
def format_lhs(_sys_calls, "z"), do: "current z position"
def format_lhs(_sys_calls, "pin" <> num), do: "Pin #{num} value"
def format_lhs(sys_calls, %{
kind: :named_pin,
args: %{pin_type: type, pin_id: pin_id}
}) do
def format_lhs(sys_calls, %{kind: :named_pin, args: %{pin_type: type, pin_id: pin_id}}) do
case named_pin(sys_calls, type, pin_id) do
%{label: label} -> label
{:error, _reason} -> "unknown left hand side"
end
end
def eval_assertion(sys_calls \\ @sys_calls, comment, expression)
when is_binary(expression) do
def eval_assertion(sys_calls \\ @sys_calls, comment, expression) when is_binary(expression) do
case sys_calls.eval_assertion(comment, expression) do
true ->
true
@ -142,13 +121,11 @@ defmodule FarmbotCeleryScript.SysCalls do
apply(@sys_calls, :log, [message, force?])
end
def sequence_init_log(sys_calls \\ @sys_calls, message)
when is_binary(message) do
def sequence_init_log(sys_calls \\ @sys_calls, message) when is_binary(message) do
apply(sys_calls, :sequence_init_log, [message])
end
def sequence_complete_log(sys_calls \\ @sys_calls, message)
when is_binary(message) do
def sequence_complete_log(sys_calls \\ @sys_calls, message) when is_binary(message) do
apply(sys_calls, :sequence_complete_log, [message])
end
@ -171,6 +148,10 @@ defmodule FarmbotCeleryScript.SysCalls do
coord_or_error(sys_calls, :coordinate, [x, y, z])
end
def dump_info(sys_calls \\ @sys_calls) do
ok_or_error(sys_calls, :dump_info, [])
end
def emergency_lock(sys_calls \\ @sys_calls) do
ok_or_error(sys_calls, :emergency_lock, [])
end
@ -179,13 +160,11 @@ defmodule FarmbotCeleryScript.SysCalls do
ok_or_error(sys_calls, :emergency_unlock, [])
end
def execute_script(sys_calls \\ @sys_calls, package, %{} = env)
when is_binary(package) do
def execute_script(sys_calls \\ @sys_calls, package, %{} = env) when is_binary(package) do
ok_or_error(sys_calls, :execute_script, [package, env])
end
def update_farmware(sys_calls \\ @sys_calls, package)
when is_binary(package) do
def update_farmware(sys_calls \\ @sys_calls, package) when is_binary(package) do
ok_or_error(sys_calls, :update_farmware, [package])
end
@ -297,6 +276,10 @@ defmodule FarmbotCeleryScript.SysCalls do
ok_or_error(sys_calls, :reboot, [])
end
def resource_update(sys_calls \\ @sys_calls, kind, id, params) do
ok_or_error(sys_calls, :resource_update, [kind, id, params])
end
def send_message(sys_calls \\ @sys_calls, kind, msg, channels) do
ok_or_error(sys_calls, :send_message, [kind, msg, channels])
end
@ -329,10 +312,6 @@ defmodule FarmbotCeleryScript.SysCalls do
ok_or_error(sys_calls, :zero, [axis])
end
def update_resource(sys_calls \\ @sys_calls, kind, id, params) do
ok_or_error(sys_calls, :update_resource, [kind, id, params])
end
defp ok_or_error(sys_calls, fun, args) do
case apply(sys_calls, fun, args) do
:ok -> :ok
@ -357,14 +336,8 @@ defmodule FarmbotCeleryScript.SysCalls do
defp coord_or_error(sys_calls, fun, args) do
case apply(sys_calls, fun, args) do
%{x: x, y: y, z: z} = coord
when is_number(x)
when is_number(y)
when is_number(z) ->
coord
error ->
or_error(sys_calls, fun, args, error)
%{x: x, y: y, z: z} = coord when is_number(x) when is_number(y) when is_number(z) -> coord
error -> or_error(sys_calls, fun, args, error)
end
end
@ -375,8 +348,7 @@ defmodule FarmbotCeleryScript.SysCalls do
end
end
defp or_error(_sys_calls, _fun, _args, {:error, reason})
when is_binary(reason) do
defp or_error(_sys_calls, _fun, _args, {:error, reason}) when is_binary(reason) do
{:error, reason}
end

View File

@ -28,6 +28,9 @@ defmodule FarmbotCeleryScript.SysCalls.Stubs do
@impl true
def coordinate(x, y, z), do: error(:coordinate, [x, y, z])
@impl true
def dump_info(), do: error(:dump_info, [])
@impl true
def emergency_lock(), do: error(:emergency_lock, [])
@ -74,32 +77,28 @@ defmodule FarmbotCeleryScript.SysCalls.Stubs do
def get_sequence(resource_id), do: error(:get_sequence, [resource_id])
@impl true
def get_toolslot_for_tool(resource_id),
do: error(:get_toolslot_for_tool, [resource_id])
def get_toolslot_for_tool(resource_id), do: error(:get_toolslot_for_tool, [resource_id])
@impl true
def home(axis, speed), do: error(:home, [axis, speed])
@impl true
def install_first_party_farmware(),
do: error(:install_first_party_farmware, [])
def install_first_party_farmware(), do: error(:install_first_party_farmware, [])
@impl true
def move_absolute(x, y, z, speed), do: error(:move_absolute, [x, y, z, speed])
@impl true
def named_pin(named_pin_type, resource_id),
do: error(:named_pin, [named_pin_type, resource_id])
def named_pin(named_pin_type, resource_id), do: error(:named_pin, [named_pin_type, resource_id])
@impl true
def nothing(), do: error(:nothing, [])
@impl true
def point(point_type, resource_id),
do: error(:point, [point_type, resource_id])
def point(point_type, resource_id), do: error(:point, [point_type, resource_id])
@impl true
def find_points_via_group(id), do: error(:find_points_via_group, [id])
def get_point_group(id_or_type), do: error(:get_point_group, [id_or_type])
@impl true
def power_off(), do: error(:power_off, [])
@ -120,8 +119,11 @@ defmodule FarmbotCeleryScript.SysCalls.Stubs do
def reboot(), do: error(:reboot, [])
@impl true
def send_message(type, message, channels),
do: error(:send_message, [type, message, channels])
def resource_update(kind, resource_id, data),
do: error(:resource_update, [kind, resource_id, data])
@impl true
def send_message(type, message, channels), do: error(:send_message, [type, message, channels])
@impl true
def set_servo_angle(pin, value), do: error(:set_servo_angle, [pin, value])
@ -130,8 +132,7 @@ defmodule FarmbotCeleryScript.SysCalls.Stubs do
def set_pin_io_mode(pin, mode), do: error(:set_pin_io_mode, [pin, mode])
@impl true
def set_user_env(env_name, env_value),
do: error(:set_user_env, [env_name, env_value])
def set_user_env(env_name, env_value), do: error(:set_user_env, [env_name, env_value])
@impl true
def sync(), do: error(:sync, [])
@ -143,16 +144,11 @@ defmodule FarmbotCeleryScript.SysCalls.Stubs do
def write_pin(pin_num, pin_mode, pin_value),
do: error(:write_pin, [pin_num, pin_mode, pin_value])
@impl true
def update_resource(kind, id, params),
do: error(:update_resource, [kind, id, params])
@impl true
def zero(axis), do: error(:zero, [axis])
@impl true
def eval_assertion(comment, expression),
do: error(:eval_assertion, [comment, expression])
def eval_assertion(comment, expression), do: error(:eval_assertion, [comment, expression])
defp error(fun, _args) do
msg = """

View File

@ -1,12 +1,7 @@
defmodule FarmbotCeleryScript.MixProject do
use Mix.Project
@version Path.join([__DIR__, "..", "VERSION"])
|> File.read!()
|> String.trim()
@elixir_version Path.join([__DIR__, "..", "ELIXIR_VERSION"])
|> File.read!()
|> String.trim()
@version Path.join([__DIR__, "..", "VERSION"]) |> File.read!() |> String.trim()
@elixir_version Path.join([__DIR__, "..", "ELIXIR_VERSION"]) |> File.read!() |> String.trim()
def project do
[
@ -44,11 +39,7 @@ defmodule FarmbotCeleryScript.MixProject do
end
def elixirc_paths(:test),
do: [
"lib",
Path.expand("./test/support"),
Path.expand("../test/support/celery_script")
]
do: ["lib", Path.expand("./test/support"), Path.expand("../test/support/celery_script")]
def elixirc_paths(_), do: ["lib"]
@ -66,9 +57,7 @@ defmodule FarmbotCeleryScript.MixProject do
{:jason, "~> 1.1"},
{:timex, "~> 3.4"},
{:excoveralls, "~> 0.10", only: [:test], targets: [:host]},
{:mimic, "~> 1.1", only: :test},
{:dialyxir, "~> 1.0.0-rc.3",
only: [:dev], targets: [:host], runtime: false},
{:dialyxir, "~> 1.0.0-rc.3", only: [:dev], targets: [:host], runtime: false},
{:ex_doc, "~> 0.21.2", only: [:dev], targets: [:host], runtime: false}
]
end

View File

@ -13,7 +13,6 @@
"makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"},
"mimic": {:hex, :mimic, "1.1.3", "3bad83d5271b4faa7bbfef587417a6605cbbc802a353395d446a1e5f46fe7115", [:mix], [], "hexpm"},
"nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm"},

View File

@ -1,4 +0,0 @@
defmodule FarmbotCeleryScript.DotPropsTest do
use ExUnit.Case
doctest FarmbotCeleryScript.DotProps, import: true
end

View File

@ -1,4 +0,0 @@
defmodule FarmbotCeleryScript.AST.FactoryTest do
use ExUnit.Case, async: true
doctest FarmbotCeleryScript.AST.Factory, import: true
end

View File

@ -1,122 +0,0 @@
defmodule FarmbotCeleryScript.CompilerGroupsTest do
use ExUnit.Case, async: true
use Mimic
alias FarmbotCeleryScript.AST
alias FarmbotCeleryScript.SysCalls.Stubs
alias FarmbotCeleryScript.Compiler.Sequence
setup :verify_on_exit!
test "compilation of point_group in parameter application" do
fake_point_ids = [4, 5, 6, 7]
expect(Stubs, :find_points_via_group, fn _ ->
%{name: "woosh", point_ids: fake_point_ids}
end)
expect(Stubs, :point, 4, fn _kind, id ->
# EDGE CASE: Handle malformed stuff by ensuring that 50% of
# points have no name.
if rem(id, 2) == 0 do
%{name: "from the test suite %%", x: 6, y: 7, z: 8}
else
%{x: 6, y: 7, z: 8}
end
end)
result = Sequence.sequence(fake_ast(), [])
length1 = 2 + length(fake_point_ids)
length2 = length(result)
assert length2 === length1
# ============================================
# ABOUT THIS (brittle) TEST:
# You should not write tests like this and
# there is a high liklihood that the code below
# will break in the future.
# This is especially true if you intend to change
# the behavior of Sequence.sequence/2.
# If you WERE NOT EXPECTING to change the behavior
# of Sequence.sequence/2 and this test fails,
# you should consider it a true failure that
# requires investigation.
# IT IS OK TO REPLACE THIS TEST WITH BETTER
# TESTS.
# ============================================
canary_actual = :crypto.hash(:sha, Macro.to_string(result))
canary_expected =
<<136, 140, 48, 226, 216, 155, 178, 103, 244, 88, 225, 146, 130, 216, 125,
72, 113, 195, 65, 1>>
# READ THE NOTE ABOVE IF THIS TEST FAILS!!!
assert canary_expected == canary_actual
end
defp fake_ast() do
%AST{
args: %{
locals: %AST{
args: %{},
body: [
%AST{
args: %{
default_value: %AST{
args: %{pointer_id: 1670, pointer_type: "Plant"},
body: [],
comment: nil,
kind: :point,
meta: nil
},
label: "parent"
},
body: [],
comment: nil,
kind: :parameter_declaration,
meta: nil
},
%AST{
args: %{
data_value: %AST{
args: %{point_group_id: 34},
body: [],
comment: nil,
kind: :point_group,
meta: nil
},
label: "parent"
},
body: [],
comment: nil,
kind: :parameter_application,
meta: nil
}
],
comment: nil,
kind: :scope_declaration,
meta: nil
},
sequence_name: "Pogo",
version: 20_180_209
},
body: [
%AST{
kind: :move_absolute,
args: %{
speed: 100,
location: %AST{kind: :identifier, args: %{label: "parent"}},
offset: %AST{
kind: :coordinate,
args: %{x: -20, y: -20, z: -20}
}
},
body: []
}
],
comment: nil,
kind: :sequence,
meta: %{sequence_name: "Pogo"}
}
end
end

View File

@ -34,10 +34,7 @@ defmodule FarmbotCeleryScript.CompilerTest do
# The compiler expects the `env` argument to be already sanatized.
# When supplying the env for this test, we need to make sure the
# `provided_by_caller` variable name is sanatized
sanatized_env = [
{IdentifierSanitizer.to_variable("provided_by_caller"), 900}
]
sanatized_env = [{IdentifierSanitizer.to_variable("provided_by_caller"), 900}]
[body_item] = Compiler.compile(sequence, sanatized_env)
assert body_item.() == 900
@ -51,9 +48,7 @@ defmodule FarmbotCeleryScript.CompilerTest do
}
]
compiled_celery_env =
Compiler.Utils.compile_params_to_function_args(celery_env, [])
compiled_celery_env = Compiler.Utils.compile_params_to_function_args(celery_env, [])
[body_item] = Compiler.compile(sequence, compiled_celery_env)
assert body_item.() == 600
end
@ -79,15 +74,12 @@ defmodule FarmbotCeleryScript.CompilerTest do
end
test "identifier sanitization" do
label = "System.cmd(\"echo\", [\"lol\"])"
label = "System.cmd(\"rm\", [\"-rf /*\"])"
value_ast = AST.Factory.new("coordinate", x: 1, y: 1, z: 1)
identifier_ast = AST.Factory.new("identifier", label: label)
parameter_application_ast =
AST.Factory.new("parameter_application",
label: label,
data_value: value_ast
)
AST.Factory.new("parameter_application", label: label, data_value: value_ast)
celery_ast = %AST{
kind: :sequence,
@ -120,19 +112,11 @@ defmodule FarmbotCeleryScript.CompilerTest do
[
fn params ->
_ = inspect(params)
unsafe_U3lzdGVtLmNtZCgiZWNobyIsIFsibG9sIl0p = FarmbotCeleryScript.SysCalls.coordinate(1, 1, 1)
better_params = %{
"System.cmd(\\"echo\\", [\\"lol\\"])" => %FarmbotCeleryScript.AST{
args: %{x: 1, y: 1, z: 1},
body: [],
comment: nil,
kind: :coordinate,
meta: nil
}
}
#{var_name} =
FarmbotCeleryScript.SysCalls.coordinate(1, 1, 1)
[fn -> unsafe_U3lzdGVtLmNtZCgiZWNobyIsIFsibG9sIl0p end]
[fn -> #{var_name} end]
end
]
""")
@ -363,114 +347,6 @@ defmodule FarmbotCeleryScript.CompilerTest do
""")
end
test "`update_resource`: " do
compiled =
"test/fixtures/mark_variable_removed.json"
|> File.read!()
|> Jason.decode!()
|> AST.decode()
|> compile()
assert compiled ==
strip_nl("""
[
fn params ->
_ = inspect(params)
unsafe_cGFyZW50 =
Keyword.get(params, :unsafe_cGFyZW50, FarmbotCeleryScript.SysCalls.coordinate(1, 2, 3))
better_params = %{}
[
fn ->
me = FarmbotCeleryScript.Compiler.UpdateResource
variable = %FarmbotCeleryScript.AST{
args: %{label: "parent"},
body: [],
comment: nil,
kind: :identifier,
meta: nil
}
update = %{"plant_stage" => "removed"}
case(variable) do
%AST{kind: :identifier} ->
args = Map.fetch!(variable, :args)
label = Map.fetch!(args, :label)
resource = Map.fetch!(better_params, label)
me.do_update(resource, update)
%AST{kind: :point} ->
me.do_update(variable.args(), update)
%AST{kind: :resource} ->
me.do_update(variable.args(), update)
res ->
raise("Resource error. Please notfiy support: \#{inspect(res)}")
end
end
]
end
]
""")
end
test "`update_resource`: Multiple fields of `resource` type." do
compiled =
"test/fixtures/update_resource_multi.json"
|> File.read!()
|> Jason.decode!()
|> AST.decode()
|> compile()
assert compiled ==
strip_nl("""
[
fn params ->
_ = inspect(params)
better_params = %{}
[
fn ->
me = FarmbotCeleryScript.Compiler.UpdateResource
variable = %FarmbotCeleryScript.AST{
args: %{resource_id: 23, resource_type: "Plant"},
body: [],
comment: nil,
kind: :resource,
meta: nil
}
update = %{"plant_stage" => "planted", "r" => 23}
case(variable) do
%AST{kind: :identifier} ->
args = Map.fetch!(variable, :args)
label = Map.fetch!(args, :label)
resource = Map.fetch!(better_params, label)
me.do_update(resource, update)
%AST{kind: :point} ->
me.do_update(variable.args(), update)
%AST{kind: :resource} ->
me.do_update(variable.args(), update)
res ->
raise("Resource error. Please notfiy support: \#{inspect(res)}")
end
end
]
end
]
""")
end
defp compile(ast) do
ast
|> Compiler.compile_ast([])

View File

@ -3,10 +3,7 @@ defmodule FarmbotCeleryScript.Corpus.NodeTest do
alias FarmbotCeleryScript.Corpus
test "inspect" do
a =
"Sequence(version, locals) [assertion, calibrate, change_ownership, check_updates, emergency_lock, emergency_unlock, execute, execute_script, factory_reset, find_home, flash_firmware, home, install_farmware, install_first_party_farmware, _if, move_absolute, move_relative, power_off, read_pin, read_status, reboot, remove_farmware, update_resource, send_message, set_servo_angle, set_user_env, sync, take_photo, toggle_pin, update_farmware, wait, write_pin, zero]"
b = inspect(Corpus.sequence())
assert a == b
assert "Sequence(version, locals) [calibrate, change_ownership, check_updates, dump_info, emergency_lock, emergency_unlock, execute, execute_script, factory_reset, find_home, flash_firmware, home, install_farmware, install_first_party_farmware, _if, move_absolute, move_relative, power_off, read_pin, read_status, reboot, remove_farmware, resource_update, send_message, set_servo_angle, set_user_env, sync, take_photo, toggle_pin, update_farmware, wait, write_pin, zero]" =
inspect(Corpus.sequence())
end
end

View File

@ -1,35 +1,33 @@
defmodule FarmbotCeleryScript.SchedulerTest do
use ExUnit.Case
use Mimic
alias FarmbotCeleryScript.{Scheduler, AST}
alias FarmbotCeleryScript.SysCalls.Stubs
import ExUnit.CaptureLog
alias Farmbot.TestSupport.CeleryScript.TestSysCalls
setup :set_mimic_global
setup :verify_on_exit!
test "schedules a sequence to run in the future" do
expect(Stubs, :read_pin, 1, fn _num, _mode ->
23
end)
{:ok, sch} =
Scheduler.start_link([registry_name: :"#{:random.uniform()}"], [])
setup do
{:ok, shim} = TestSysCalls.checkout()
{:ok, sch} = Scheduler.start_link([registry_name: :"#{:random.uniform()}"], [])
[shim: shim, sch: sch]
end
test "schedules a sequence to run in the future", %{sch: sch} do
ast =
AST.Factory.new()
|> AST.Factory.rpc_request("hello world")
|> AST.Factory.read_pin(9, 0)
scheduled_time = DateTime.utc_now() |> DateTime.add(100, :millisecond)
# msg = "[info] Next execution is ready for execution: now"
{:ok, _} = Scheduler.schedule(sch, ast, scheduled_time, %{})
pid = self()
:ok =
TestSysCalls.handle(TestSysCalls, fn
:read_pin, args ->
send(pid, {:read_pin, args})
1
end)
scheduled_time = DateTime.utc_now() |> DateTime.add(100, :millisecond)
{:ok, _} = Scheduler.schedule(sch, ast, scheduled_time, %{})
# Hack to force the scheduler to checkup instead of waiting the normal 15 seconds
assert capture_log(fn ->
send(sch, :checkup)
# Sorry.
Process.sleep(1100)
end) =~ "[info] Next execution is ready for execution: now"
send(sch, :checkup)
assert_receive {:read_pin, [9, 0]}, 1000
end
end

View File

@ -1,240 +1,178 @@
defmodule FarmbotCeleryScript.SysCallsTest do
use ExUnit.Case, async: false
use Mimic
alias Farmbot.TestSupport.CeleryScript.TestSysCalls
alias FarmbotCeleryScript.{AST, SysCalls}
alias FarmbotCeleryScript.{
SysCalls,
SysCalls.Stubs,
AST
}
setup :verify_on_exit!
test "point, OK" do
expect(Stubs, :point, 1, fn _kind, 1 ->
%{x: 100, y: 200, z: 300}
end)
result1 = SysCalls.point(Stubs, "Peripheral", 1)
assert %{x: 100, y: 200, z: 300} == result1
setup do
{:ok, shim} = TestSysCalls.checkout()
[shim: shim]
end
test "point, NO" do
expect(Stubs, :point, 1, fn _kind, 0 ->
:whatever
end)
test "point", %{shim: shim} do
:ok = shim_fun_ok(shim, %{x: 100, y: 200, z: 300})
assert %{x: 100, y: 200, z: 300} = SysCalls.point(TestSysCalls, "Peripheral", 1)
assert_receive {:point, ["Peripheral", 1]}
boom = fn -> SysCalls.point(Stubs, "Peripheral", 0) end
assert_raise FarmbotCeleryScript.RuntimeError, boom
:ok = shim_fun_error(shim, "point error")
assert {:error, "point error"} == SysCalls.point(TestSysCalls, "Peripheral", 1)
end
test "point groups failure" do
expect(Stubs, :find_points_via_group, 1, fn _id ->
:whatever
end)
test "move_absolute", %{shim: shim} do
:ok = shim_fun_ok(shim)
assert :ok = SysCalls.move_absolute(TestSysCalls, 1, 2, 3, 4)
assert_receive {:move_absolute, [1, 2, 3, 4]}
boom = fn -> SysCalls.find_points_via_group(Stubs, :something_else) end
assert_raise FarmbotCeleryScript.RuntimeError, boom
:ok = shim_fun_error(shim, "move failed!")
assert {:error, "move failed!"} == SysCalls.move_absolute(TestSysCalls, 1, 2, 3, 4)
end
test "point groups success" do
expect(Stubs, :find_points_via_group, 1, fn _id ->
%{point_ids: [1, 2, 3]}
end)
test "get current positions", %{shim: shim} do
:ok = shim_fun_ok(shim, 100.00)
assert 100.00 = SysCalls.get_current_x(TestSysCalls)
assert 100.00 = SysCalls.get_current_y(TestSysCalls)
assert 100.00 = SysCalls.get_current_z(TestSysCalls)
pg = %{point_ids: [1, 2, 3]}
result = SysCalls.find_points_via_group(Stubs, 456)
assert result == pg
assert_receive {:get_current_x, []}
assert_receive {:get_current_y, []}
assert_receive {:get_current_z, []}
:ok = shim_fun_error(shim, "firmware error")
assert {:error, "firmware error"} == SysCalls.get_current_x(TestSysCalls)
assert {:error, "firmware error"} == SysCalls.get_current_y(TestSysCalls)
assert {:error, "firmware error"} == SysCalls.get_current_z(TestSysCalls)
end
test "move_absolute, OK" do
expect(Stubs, :move_absolute, 1, fn 1, 2, 3, 4 ->
:ok
end)
test "write_pin", %{shim: shim} do
:ok = shim_fun_ok(shim)
assert :ok = SysCalls.write_pin(TestSysCalls, 1, 0, 1)
assert :ok = SysCalls.write_pin(TestSysCalls, %{type: "boxled", id: 4}, 0, 1)
assert :ok = SysCalls.write_pin(TestSysCalls, %{type: "boxled", id: 3}, 1, 123)
assert :ok = SysCalls.move_absolute(Stubs, 1, 2, 3, 4)
assert_receive {:write_pin, [1, 0, 1]}
assert_receive {:write_pin, [%{type: "boxled", id: 4}, 0, 1]}
assert_receive {:write_pin, [%{type: "boxled", id: 3}, 1, 123]}
:ok = shim_fun_error(shim, "firmware error")
assert {:error, "firmware error"} == SysCalls.write_pin(TestSysCalls, 1, 0, 1)
end
test "move_absolute, NO" do
expect(Stubs, :move_absolute, 1, fn 1, 2, 3, 4 ->
{:error, "move failed!"}
end)
test "read_pin", %{shim: shim} do
:ok = shim_fun_ok(shim, 1)
assert 1 == SysCalls.read_pin(TestSysCalls, 10, 0)
assert 1 == SysCalls.read_pin(TestSysCalls, 77, nil)
assert_receive {:read_pin, [10, 0]}
assert_receive {:read_pin, [77, nil]}
assert {:error, "move failed!"} ==
SysCalls.move_absolute(Stubs, 1, 2, 3, 4)
:ok = shim_fun_error(shim, "firmware error")
assert {:error, "firmware error"} == SysCalls.read_pin(TestSysCalls, 1, 0)
end
test "get positions, OK" do
expect(Stubs, :get_current_x, 1, fn -> 100.00 end)
expect(Stubs, :get_current_y, 1, fn -> 200.00 end)
expect(Stubs, :get_current_z, 1, fn -> 300.00 end)
assert 100.00 = SysCalls.get_current_x(Stubs)
assert 200.00 = SysCalls.get_current_y(Stubs)
assert 300.00 = SysCalls.get_current_z(Stubs)
test "wait", %{shim: shim} do
:ok = shim_fun_ok(shim)
assert :ok = SysCalls.wait(TestSysCalls, 1000)
assert_receive {:wait, [1000]}
end
test "get positions, KO" do
expect(Stubs, :get_current_x, 1, fn -> {:error, "L"} end)
expect(Stubs, :get_current_y, 1, fn -> {:error, "O"} end)
expect(Stubs, :get_current_z, 1, fn -> {:error, "L"} end)
assert {:error, "L"} == SysCalls.get_current_x(Stubs)
assert {:error, "O"} == SysCalls.get_current_y(Stubs)
assert {:error, "L"} == SysCalls.get_current_z(Stubs)
end
test "write_pin" do
err = {:error, "firmware error?"}
expect(Stubs, :write_pin, 4, fn pin_num, _, _ ->
if pin_num == 66 do
err
else
:ok
end
end)
assert :ok = SysCalls.write_pin(Stubs, 1, 0, 1)
assert :ok = SysCalls.write_pin(Stubs, %{type: "boxled", id: 4}, 0, 1)
assert :ok = SysCalls.write_pin(Stubs, %{type: "boxled", id: 3}, 1, 123)
assert err == SysCalls.write_pin(Stubs, 66, 0, 1)
end
test "read_pin" do
expect(Stubs, :read_pin, 3, fn num, _mode ->
if num == 1 do
{:error, "firmware error"}
else
num * 2
end
end)
assert 20 == SysCalls.read_pin(Stubs, 10, 0)
assert 30 == SysCalls.read_pin(Stubs, 15, nil)
assert {:error, "firmware error"} == SysCalls.read_pin(Stubs, 1, 0)
end
test "wait" do
expect(Stubs, :wait, fn ms ->
if ms == 1000 do
:ok
end
end)
assert :ok = SysCalls.wait(Stubs, 1000)
end
test "named_pin" do
err = {:error, "error finding resource"}
expect(Stubs, :named_pin, 5, fn kind, num ->
hmm = {kind, num}
case hmm do
{"Peripheral", 5} -> 44
{"Sensor", 1999} -> 55
{"BoxLed", 3} -> %{type: "BoxLed", id: 3}
{"BoxLed", 4} -> %{type: "BoxLed", id: 4}
{"Peripheral", 888} -> err
end
end)
test "named_pin", %{shim: shim} do
# Peripheral and Sensor are on the Arduino
assert 44 == SysCalls.named_pin(Stubs, "Peripheral", 5)
assert 55 == SysCalls.named_pin(Stubs, "Sensor", 1999)
:ok = shim_fun_ok(shim, 44)
assert 44 == SysCalls.named_pin(TestSysCalls, "Peripheral", 5)
assert 44 == SysCalls.named_pin(TestSysCalls, "Sensor", 1999)
# BoxLed is on the GPIO
:ok = shim_fun_ok(shim, %{type: "BoxLed", id: 3})
assert %{type: "BoxLed", id: 3} == SysCalls.named_pin(TestSysCalls, "BoxLed", 3)
assert %{type: "BoxLed", id: 3} ==
SysCalls.named_pin(Stubs, "BoxLed", 3)
:ok = shim_fun_ok(shim, %{type: "BoxLed", id: 4})
assert %{type: "BoxLed", id: 4} == SysCalls.named_pin(TestSysCalls, "BoxLed", 4)
assert %{type: "BoxLed", id: 4} ==
SysCalls.named_pin(Stubs, "BoxLed", 4)
assert_receive {:named_pin, ["Peripheral", 5]}
assert_receive {:named_pin, ["Sensor", 1999]}
assert_receive {:named_pin, ["BoxLed", 3]}
assert_receive {:named_pin, ["BoxLed", 4]}
assert err == SysCalls.named_pin(Stubs, "Peripheral", 888)
:ok = shim_fun_error(shim, "error finding resource")
assert {:error, "error finding resource"} ==
SysCalls.named_pin(TestSysCalls, "Peripheral", 888)
end
test "send_message" do
err = {:error, "email machine broke"}
test "send_message", %{shim: shim} do
:ok = shim_fun_ok(shim)
assert :ok = SysCalls.send_message(TestSysCalls, "success", "hello world", ["email"])
assert_receive {:send_message, ["success", "hello world", ["email"]]}
expect(Stubs, :send_message, 2, fn type, _msg, _chans ->
if type == "error" do
err
else
:ok
end
end)
:ok = shim_fun_error(shim, "email machine broke")
assert :ok =
SysCalls.send_message(Stubs, "success", "hello world", ["email"])
assert err ==
SysCalls.send_message(Stubs, "error", "goodbye world", ["email"])
assert {:error, "email machine broke"} ==
SysCalls.send_message(TestSysCalls, "error", "goodbye world", ["email"])
end
test "find_home" do
err = {:error, "home lost"}
test "find_home", %{shim: shim} do
:ok = shim_fun_ok(shim)
assert :ok = SysCalls.find_home(TestSysCalls, "x")
assert_receive {:find_home, ["x"]}
expect(Stubs, :find_home, 2, fn axis ->
if axis == "x" do
:ok
else
err
end
end)
:ok = shim_fun_error(shim, "home lost")
assert :ok = SysCalls.find_home(Stubs, "x")
assert err == SysCalls.find_home(Stubs, "z")
assert {:error, "home lost"} == SysCalls.find_home(TestSysCalls, "x")
end
test "execute_script" do
err = {:error, "not installed"}
test "execute_script", %{shim: shim} do
:ok = shim_fun_ok(shim)
assert :ok = SysCalls.execute_script(TestSysCalls, "take-photo", %{})
assert_receive {:execute_script, ["take-photo", %{}]}
expect(Stubs, :execute_script, 2, fn "take-photo", env ->
if Map.get(env, :error) do
err
else
:ok
end
end)
:ok = shim_fun_error(shim, "not installed")
assert :ok = SysCalls.execute_script(Stubs, "take-photo", %{})
assert err == SysCalls.execute_script(Stubs, "take-photo", %{error: true})
assert {:error, "not installed"} == SysCalls.execute_script(TestSysCalls, "take-photo", %{})
end
test "set_servo_angle errors" do
error = {:error, "boom"}
test "set_servo_angle errors", %{shim: shim} do
:ok = shim_fun_ok(shim)
arg0 = [5, 40]
assert :ok = SysCalls.set_servo_angle(TestSysCalls, "set_servo_angle", arg0)
assert_receive {:set_servo_angle, arg0}
expect(Stubs, :set_servo_angle, 2, fn num, _val ->
if num == 5 do
:ok
else
error
end
end)
assert error == SysCalls.set_servo_angle(Stubs, 40, -5)
assert :ok == SysCalls.set_servo_angle(Stubs, 5, 40)
arg1 = [40, -5]
:ok = shim_fun_error(shim, "boom")
assert {:error, "boom"} == SysCalls.set_servo_angle(TestSysCalls, "set_servo_angle", arg1)
end
test "get_sequence" do
nothing = %AST{
kind: "nothing",
args: {},
body: []
}
test "get_sequence", %{shim: shim} do
:ok =
shim_fun_ok(shim, %AST{
kind: :sequence,
args: %{locals: %AST{kind: :scope_declaration, args: %{}}}
})
err = {:error, "sequence not found"}
assert %{} = SysCalls.get_sequence(TestSysCalls, 123)
assert_receive {:get_sequence, [123]}
expect(Stubs, :get_sequence, 2, fn sequence_id ->
if sequence_id == 321 do
err
else
nothing
end
end)
:ok = shim_fun_error(shim, "sequence not found")
assert nothing == SysCalls.get_sequence(Stubs, 123)
assert err == SysCalls.get_sequence(Stubs, 321)
assert {:error, "sequence not found"} == SysCalls.get_sequence(TestSysCalls, 123)
end
def shim_fun_ok(shim, val \\ :ok) do
pid = self()
:ok =
TestSysCalls.handle(shim, fn kind, args ->
send(pid, {kind, args})
val
end)
end
def shim_fun_error(shim, val) when is_binary(val) do
:ok =
TestSysCalls.handle(shim, fn _kind, _args ->
{:error, val}
end)
end
end

View File

@ -1,14 +1,12 @@
defmodule FarmbotCeleryScriptTest do
use ExUnit.Case, async: true
use Mimic
alias FarmbotCeleryScript.AST
alias FarmbotCeleryScript.SysCalls.Stubs
alias Farmbot.TestSupport.CeleryScript.TestSysCalls
import ExUnit.CaptureIO
import ExUnit.CaptureLog
setup :verify_on_exit!
setup do
{:ok, _shim} = TestSysCalls.checkout()
:ok
end
test "uses default values when no parameter is found" do
sequence_ast =
@ -26,7 +24,7 @@ defmodule FarmbotCeleryScriptTest do
label: "foo",
default_value: %{
kind: :coordinate,
args: %{x: 12, y: 11, z: 10}
args: %{x: 129, y: 129, z: 129}
}
}
}
@ -54,18 +52,19 @@ defmodule FarmbotCeleryScriptTest do
me = self()
expect(Stubs, :coordinate, 2, fn x, y, z ->
%{x: x, y: y, z: z}
end)
:ok =
TestSysCalls.handle(TestSysCalls, fn
:move_absolute, args ->
send(me, {:move_absolute, args})
:ok
expect(Stubs, :move_absolute, 1, fn _x, _y, _z, _s ->
:ok
end)
:coordinate, [x, y, z] ->
%{x: x, y: y, z: z}
end)
capture_log(fn ->
result = FarmbotCeleryScript.execute(sequence_ast, me)
assert :ok == result
end) =~ "[error] CeleryScript syscall stubbed: log"
_ = FarmbotCeleryScript.execute(sequence_ast, me)
assert_receive {:step_complete, ^me, :ok}
assert_receive {:move_absolute, [129, 129, 129, 921]}
end
test "syscall errors" do
@ -79,12 +78,13 @@ defmodule FarmbotCeleryScriptTest do
}
|> AST.decode()
expect(Stubs, :read_pin, 1, fn _, _ -> {:error, "failed to read pin!"} end)
result = FarmbotCeleryScript.execute(execute_ast, execute_ast)
assert {:error, "failed to read pin!"} = result
:ok =
TestSysCalls.handle(TestSysCalls, fn
:read_pin, _ -> {:error, "failed to read pin!"}
end)
assert_receive {:step_complete, ^execute_ast,
{:error, "failed to read pin!"}}
assert {:error, "failed to read pin!"} = FarmbotCeleryScript.execute(execute_ast, execute_ast)
assert_receive {:step_complete, ^execute_ast, {:error, "failed to read pin!"}}
end
test "regular exceptions still occur" do
@ -98,17 +98,12 @@ defmodule FarmbotCeleryScriptTest do
}
|> AST.decode()
expect(Stubs, :read_pin, fn _, _ ->
raise("big oops")
end)
io =
capture_io(:stderr, fn ->
assert {:error, "big oops"} ==
FarmbotCeleryScript.execute(execute_ast, execute_ast)
:ok =
TestSysCalls.handle(TestSysCalls, fn
:read_pin, _ -> raise("big oops")
end)
assert io =~ "CeleryScript Exception"
assert {:error, "big oops"} == FarmbotCeleryScript.execute(execute_ast, execute_ast)
assert_receive {:step_complete, ^execute_ast, {:error, "big oops"}}
end
end

View File

@ -1,48 +0,0 @@
{
"args": {
"locals": {
"args": {},
"body": [
{
"args": {
"default_value": {
"args": {
"x": 1,
"y": 2,
"z": 3
},
"kind": "coordinate"
},
"label": "parent"
},
"kind": "parameter_declaration"
}
],
"kind": "scope_declaration"
},
"version": -999
},
"body": [
{
"args": {
"resource": {
"args": {
"label": "parent"
},
"kind": "identifier"
}
},
"body": [
{
"args": {
"label": "meta.my_prop",
"value": "whatever"
},
"kind": "pair"
}
],
"kind": "update_resource"
}
],
"kind": "sequence"
}

View File

@ -1,48 +0,0 @@
{
"args": {
"locals": {
"args": {},
"body": [
{
"args": {
"default_value": {
"args": {
"x": 1,
"y": 2,
"z": 3
},
"kind": "coordinate"
},
"label": "parent"
},
"kind": "parameter_declaration"
}
],
"kind": "scope_declaration"
},
"version": -999
},
"body": [
{
"args": {
"resource": {
"args": {
"label": "parent"
},
"kind": "identifier"
}
},
"body": [
{
"args": {
"label": "plant_stage",
"value": "removed"
},
"kind": "pair"
}
],
"kind": "update_resource"
}
],
"kind": "sequence"
}

View File

@ -1,34 +0,0 @@
{
"args": {
"locals": {
"args": {},
"body": [],
"kind": "scope_declaration"
},
"version": -999
},
"body": [
{
"args": {
"resource": {
"args": {
"resource_id": 0,
"resource_type": "Device"
},
"kind": "resource"
}
},
"body": [
{
"args": {
"label": "mounted_tool_id",
"value": 12161
},
"kind": "pair"
}
],
"kind": "update_resource"
}
],
"kind": "sequence"
}

View File

@ -1,41 +0,0 @@
{
"args": {
"locals": {
"args": {},
"body": [],
"kind": "scope_declaration"
},
"version": -999
},
"body": [
{
"args": {
"resource": {
"args": {
"resource_id": 23,
"resource_type": "Plant"
},
"kind": "resource"
}
},
"body": [
{
"args": {
"label": "plant_stage",
"value": "planted"
},
"kind": "pair"
},
{
"args": {
"label": "r",
"value": 23
},
"kind": "pair"
}
],
"kind": "update_resource"
}
],
"kind": "sequence"
}

View File

@ -1,3 +1 @@
Application.ensure_all_started(:mimic)
Mimic.copy(FarmbotCeleryScript.SysCalls.Stubs)
ExUnit.start()

View File

@ -1,6 +1,5 @@
[
import_deps: [:ecto],
line_length: 80,
inputs: [
"*.{ex,exs}",
"{config,priv,test}/**/*.{ex,exs}",

View File

@ -1,52 +1,51 @@
use Mix.Config
config :farmbot_core, FarmbotCore.AssetWorker.FarmbotCore.Asset.FarmEvent,
checkup_time_ms: 10_000
config :farmbot_core, FarmbotCore.AssetWorker.FarmbotCore.Asset.FarmEvent, checkup_time_ms: 10_000
config :farmbot_core, FarmbotCore.AssetWorker.FarmbotCore.Asset.RegimenInstance,
checkup_time_ms: 10_000
config :farmbot_core,
FarmbotCore.AssetWorker.FarmbotCore.Asset.FarmwareInstallation,
error_retry_time_ms: 30_000,
install_dir: "/tmp/farmware"
config :farmbot_core, FarmbotCore.AssetWorker.FarmbotCore.Asset.FarmwareInstallation,
error_retry_time_ms: 30_000,
install_dir: "/tmp/farmware"
config :farmbot_core, FarmbotCore.FarmwareRuntime,
runtime_dir: "/tmp/farmware_runtime"
config :farmbot_core, FarmbotCore.FarmwareRuntime, runtime_dir: "/tmp/farmware_runtime"
config :farmbot_core, FarmbotCore.AssetWorker.FarmbotCore.Asset.PinBinding,
gpio_handler: FarmbotCore.PinBindingWorker.StubGPIOHandler,
error_retry_time_ms: 30_000
config :farmbot_core,
Elixir.FarmbotCore.AssetWorker.FarmbotCore.Asset.PublicKey,
ssh_handler: FarmbotCore.PublicKeyHandler.StubSSHHandler
config :farmbot_core, Elixir.FarmbotCore.AssetWorker.FarmbotCore.Asset.PublicKey,
ssh_handler: FarmbotCore.PublicKeyHandler.StubSSHHandler
config :farmbot_core, FarmbotCore.AssetMonitor, checkup_time_ms: 30_000
config :farmbot_core, FarmbotCore.Leds,
gpio_handler: FarmbotCore.Leds.StubHandler
config :farmbot_core, FarmbotCore.Leds, gpio_handler: FarmbotCore.Leds.StubHandler
config :farmbot_core, FarmbotCore.JSON,
json_parser: FarmbotCore.JSON.JasonParser
config :farmbot_core, FarmbotCore.JSON, json_parser: FarmbotCore.JSON.JasonParser
config :farmbot_core, FarmbotCore.BotState.FileSystem,
root_dir: "/tmp/farmbot",
sleep_time: 200
config :farmbot_core, FarmbotCore.EctoMigrator,
expected_fw_versions: ["6.4.2.F", "6.4.2.R", "6.4.2.G"],
default_firmware_io_logs: false,
default_server: "https://my.farm.bot",
default_dns_name: "my.farm.bot",
default_ntp_server_1: "0.pool.ntp.org",
default_ntp_server_2: "1.pool.ntp.org",
default_currently_on_beta:
String.contains?(
to_string(:os.cmd('git rev-parse --abbrev-ref HEAD')),
"beta"
)
String.contains?(to_string(:os.cmd('git rev-parse --abbrev-ref HEAD')), "beta")
config :farmbot_firmware, FarmbotFirmware, reset: FarmbotCore.FirmwareResetter
config :farmbot_core, FarmbotCore.FirmwareTTYDetector, expected_names: []
config :farmbot_core, FarmbotCore.FirmwareOpenTask, attempt_threshold: 5
config :farmbot_firmware, FarmbotFirmware, reset: FarmbotFirmware.NullReset
config :farmbot_core, FarmbotCore.AssetWorker.FarmbotCore.Asset.FbosConfig,
firmware_flash_attempt_threshold: 5
import_config "ecto.exs"
import_config "logger.exs"

View File

@ -2,3 +2,8 @@ use Mix.Config
config :farmbot_celery_script, FarmbotCeleryScript.SysCalls,
sys_calls: FarmbotCeleryScript.SysCalls.Stubs
config :farmbot_core, FarmbotCore.FirmwareOpenTask, attempt_threshold: 5
config :farmbot_core, FarmbotCore.AssetWorker.FarmbotCore.Asset.FbosConfig,
firmware_flash_attempt_threshold: 5

View File

@ -2,11 +2,7 @@ use Mix.Config
config :ecto, json_library: FarmbotCore.JSON
config :farmbot_core,
ecto_repos: [
FarmbotCore.Config.Repo,
FarmbotCore.Logger.Repo,
FarmbotCore.Asset.Repo
]
ecto_repos: [FarmbotCore.Config.Repo, FarmbotCore.Logger.Repo, FarmbotCore.Asset.Repo]
config :farmbot_core, FarmbotCore.Config.Repo,
adapter: Sqlite.Ecto2,

View File

@ -1,36 +1,18 @@
use Mix.Config
config :logger, level: :warn
# must be lower than other timers
# To ensure other timers have time to timeout
config :farmbot_core, FarmbotCore.AssetMonitor, checkup_time_ms: 500
config :farmbot_core, FarmbotCore.AssetWorker.FarmbotCore.Asset.FarmEvent,
checkup_time_ms: 1000
config :farmbot_core, FarmbotCore.AssetWorker.FarmbotCore.Asset.FarmEvent, checkup_time_ms: 1000
config :farmbot_core, FarmbotCore.AssetWorker.FarmbotCore.Asset.RegimenInstance,
checkup_time_ms: 1000
config :farmbot_celery_script, FarmbotCeleryScript.SysCalls,
sys_calls: FarmbotCeleryScript.SysCalls.Stubs
sys_calls: Farmbot.TestSupport.CeleryScript.TestSysCalls
config :farmbot_core, FarmbotCore.FirmwareOpenTask, attempt_threshold: 0
config :farmbot_core, FarmbotCore.AssetWorker.FarmbotCore.Asset.FbosConfig,
firmware_flash_attempt_threshold: 0
if Mix.env() == :test do
config :ex_unit, capture_logs: true
mapper = fn mod -> config :farmbot_core, mod, children: [] end
list = [
FarmbotCore,
FarmbotCore.StorageSupervisor,
FarmbotCore.Asset.Supervisor,
FarmbotCore.BotState.Supervisor,
FarmbotCore.Config.Supervisor,
FarmbotCore.Logger.Supervisor
]
Enum.map(list, mapper)
end

View File

@ -1,6 +0,0 @@
{
"coverage_options": {
"treat_no_relevant_lines_as_covered": true,
"minimum_coverage": 25
}
}

View File

@ -14,11 +14,9 @@ defmodule FarmbotCore do
def start(_, args), do: Supervisor.start_link(__MODULE__, args, name: __MODULE__)
def init([]) do
Supervisor.init(children(), [strategy: :one_for_one])
end
def children do
default = [
children = [
FarmbotCore.DepTracker,
FarmbotCore.Leds,
FarmbotCore.EctoMigrator,
FarmbotCore.BotState.Supervisor,
@ -27,13 +25,9 @@ defmodule FarmbotCore do
FarmbotCore.FirmwareOpenTask,
FarmbotCore.FirmwareEstopTimer,
# Also error handling for a transport not starting ?
{FarmbotFirmware,
transport: FarmbotFirmware.StubTransport,
side_effects: FarmbotCore.FirmwareSideEffects,
reset: FarmbotCore.FirmwareResetter},
{FarmbotFirmware, transport: FarmbotFirmware.StubTransport, side_effects: FarmbotCore.FirmwareSideEffects},
FarmbotCeleryScript.Scheduler
]
config = (Application.get_env(:farmbot_ext, __MODULE__) || [])
Keyword.get(config, :children, default)
Supervisor.init(children, [strategy: :one_for_one])
end
end

View File

@ -6,15 +6,16 @@ defmodule FarmbotCore.Asset do
"""
alias FarmbotCore.Asset.{
CriteriaRetriever,
Repo,
Device,
DeviceCert,
FarmEvent,
DiagnosticDump,
FarmwareEnv,
FirstPartyFarmware,
FarmwareInstallation,
FarmEvent,
FbosConfig,
FirmwareConfig,
FirstPartyFarmware,
Peripheral,
PinBinding,
Point,
@ -22,11 +23,10 @@ defmodule FarmbotCore.Asset do
PublicKey,
Regimen,
RegimenInstance,
Repo,
Sequence,
Sensor,
SensorReading,
Sequence,
Tool,
Tool
}
alias FarmbotCore.AssetSupervisor
@ -54,7 +54,6 @@ defmodule FarmbotCore.Asset do
if device = Repo.get_by(Device, id: id) do
Repo.delete!(device)
end
:ok
end
@ -62,6 +61,11 @@ defmodule FarmbotCore.Asset do
## Begin FarmEvent
@doc "Returns all FarmEvents"
def list_farm_events do
Repo.all(FarmEvent)
end
def new_farm_event!(params) do
%FarmEvent{}
|> FarmEvent.changeset(params)
@ -74,14 +78,13 @@ defmodule FarmbotCore.Asset do
end
def update_farm_event!(farm_event, params) do
farm_event =
farm_event
|> FarmEvent.changeset(params)
farm_event =
farm_event |>
FarmEvent.changeset(params)
|> Repo.update!()
if farm_event.executable_type == "Regimen" do
regimen_instance = get_regimen_instance(farm_event)
if regimen_instance do
regimen_instance
|> Repo.preload([:farm_event, :regimen])
@ -108,11 +111,9 @@ defmodule FarmbotCore.Asset do
def get_farm_event_execution(%FarmEvent{} = farm_event, scheduled_at) do
Repo.one(
from(e in FarmEvent.Execution,
where:
e.farm_event_local_id == ^farm_event.local_id and
e.scheduled_at == ^scheduled_at
)
from e in FarmEvent.Execution,
where: e.farm_event_local_id == ^farm_event.local_id
and e.scheduled_at == ^scheduled_at
)
end
@ -148,7 +149,6 @@ defmodule FarmbotCore.Asset do
if fbos_config = Repo.get_by(FbosConfig, id: id) do
Repo.delete!(fbos_config)
end
:ok
end
@ -177,7 +177,6 @@ defmodule FarmbotCore.Asset do
if firmware_config = Repo.get_by(FirmwareConfig, id: id) do
Repo.delete!(firmware_config)
end
:ok
end
@ -193,19 +192,12 @@ defmodule FarmbotCore.Asset do
end
def get_regimen_instance(%FarmEvent{} = farm_event) do
regimen = Repo.one(from(r in Regimen, where: r.id == ^farm_event.executable_id))
regimen &&
Repo.one(
from(ri in RegimenInstance,
where: ri.regimen_id == ^regimen.local_id and ri.farm_event_id == ^farm_event.local_id
)
)
regimen = Repo.one(from r in Regimen, where: r.id == ^farm_event.executable_id)
regimen && Repo.one(from ri in RegimenInstance, where: ri.regimen_id == ^regimen.local_id and ri.farm_event_id == ^farm_event.local_id)
end
def new_regimen_instance!(%FarmEvent{} = farm_event, params \\ %{}) do
regimen = Repo.one!(from(r in Regimen, where: r.id == ^farm_event.executable_id))
regimen = Repo.one!(from r in Regimen, where: r.id == ^farm_event.executable_id)
RegimenInstance.changeset(%RegimenInstance{}, params)
|> Ecto.Changeset.put_assoc(:regimen, regimen)
|> Ecto.Changeset.put_assoc(:farm_event, farm_event)
@ -215,7 +207,7 @@ defmodule FarmbotCore.Asset do
def delete_regimen_instance!(%RegimenInstance{} = ri) do
Repo.delete!(ri)
end
def add_execution_to_regimen_instance!(%RegimenInstance{} = ri, params \\ %{}) do
%RegimenInstance.Execution{}
|> RegimenInstance.Execution.changeset(params)
@ -225,11 +217,9 @@ defmodule FarmbotCore.Asset do
def get_regimen_instance_execution(%RegimenInstance{} = ri, scheduled_at) do
Repo.one(
from(e in RegimenInstance.Execution,
where:
e.regimen_instance_local_id == ^ri.local_id and
e.scheduled_at == ^scheduled_at
)
from e in RegimenInstance.Execution,
where: e.regimen_instance_local_id == ^ri.local_id
and e.scheduled_at == ^scheduled_at
)
end
@ -251,30 +241,14 @@ defmodule FarmbotCore.Asset do
end
def update_point(point, params) do
# TODO: RC 8 MAY 2020 - We need to hard refresh the point.
# The CSVM appears to be caching resources. This leads
# to problems when a user runs a sequence that has two
# MARK AS steps.
# NOTE: Updating the `meta` attribute is a _replace_ action
# by default, not a merge action.
# MORE NOTES: Mixed keys (symbol vs. string) will crash this FN.
# Let's just stringify everything...
new_meta = params[:meta] || params["meta"] || %{}
old_meta = point.meta || %{}
updated_meta = Map.merge(old_meta, new_meta)
clean_params = params
|> Map.merge(%{meta: updated_meta})
|> Enum.map(fn {k, v} -> {"#{k}", v} end)
|> Map.new()
Repo.get_by(Point, id: point.id)
|> Point.changeset(clean_params)
point
|> Point.changeset(params)
|> Repo.update()
end
@doc "Returns all points matching Point.pointer_type"
def get_all_points_by_type(type, order_by \\ "random") do
from(p in Point, where: p.pointer_type == ^type and is_nil(p.discarded_at))
(from p in Point, where: p.pointer_type == ^type and is_nil(p.discarded_at))
|> Repo.all()
|> sort_points(order_by)
end
@ -283,7 +257,7 @@ defmodule FarmbotCore.Asset do
points
|> Enum.group_by(&group_points_by(&1, order_by))
|> Enum.sort(&group_sort(&1, &2, order_by))
|> Enum.map(fn {_group_index, group} -> Enum.sort(group, &sort_points(&1, &2, order_by)) end)
|> Enum.map(fn({_group_index, group}) -> Enum.sort(group, &sort_points(&1, &2, order_by)) end)
|> List.flatten()
end
@ -298,6 +272,7 @@ defmodule FarmbotCore.Asset do
def group_sort({lgroup, _}, {rgroup, _}, "yx_descending"), do: lgroup >= rgroup
def group_sort(_, _, "random"), do: Enum.random([true, false])
def sort_points(%{y: ly}, %{y: ry}, "xy_ascending"), do: ly <= ry
def sort_points(%{y: ly}, %{y: ry}, "xy_descending"), do: ly >= ry
def sort_points(%{x: lx}, %{x: rx}, "yx_ascending"), do: lx <= rx
@ -310,15 +285,15 @@ defmodule FarmbotCore.Asset do
def get_point_group(params) do
case Repo.get_by(PointGroup, params) do
nil ->
nil ->
nil
%{sort_type: nil} = group ->
group
%{sort_type: nil} = group ->
group
%{point_ids: unsorted, sort_type: sort_by} = point_group ->
sorted =
Repo.all(from(p in Point, where: p.id in ^unsorted))
sorted =
Repo.all(from p in Point, where: p.id in ^unsorted)
|> sort_points(sort_by)
|> Enum.map(&Map.fetch!(&1, :id))
@ -326,30 +301,6 @@ defmodule FarmbotCore.Asset do
end
end
def find_points_via_group(id) do
case Repo.get_by(PointGroup, id: id) do
%{id: _id, sort_type: sort_by} = point_group ->
# I don't like this because it makes the code
# harder to understand.
# We are essentially patching the value of
# point_group.point_ids with additional IDs.
# Keep this in mind when debugging sequences
# that deal with point groups- the point_ids
# value is not a reflection of what is in
# the DB / API.
sorted = CriteriaRetriever.run(point_group)
|> sort_points(sort_by || "xy_ascending")
|> Enum.map(fn point -> point.id end)
%{ point_group | point_ids: sorted }
other ->
# Swallow all other errors
a = inspect(id)
b = inspect(other)
Logger.debug("Unexpected point group #{a} #{b}")
nil
end
end
def new_point_group!(params) do
%PointGroup{}
|> PointGroup.changeset(params)
@ -357,13 +308,13 @@ defmodule FarmbotCore.Asset do
end
def update_point_group!(point_group, params) do
updated =
updated =
point_group
|> PointGroup.changeset(params)
|> Repo.update!()
regimen_instances = list_regimen_instances()
farm_events = Repo.all(FarmEvent)
farm_events = list_farm_events()
# check for any matching asset using this point group.
# This is pretty recursive and probably isn't super great
@ -371,11 +322,11 @@ defmodule FarmbotCore.Asset do
for asset <- farm_events ++ regimen_instances do
# TODO(Connor) this might be worth creating a behaviour for
if uses_point_group?(asset, point_group) do
Logger.debug("#{inspect(asset)} uses PointGroup: #{inspect(point_group)}. Reindexing it.")
Logger.debug "#{inspect(asset)} uses PointGroup: #{inspect(point_group)}. Reindexing it."
FarmbotCore.AssetSupervisor.update_child(asset)
end
end
updated
end
@ -384,16 +335,14 @@ defmodule FarmbotCore.Asset do
end
def uses_point_group?(%FarmEvent{body: body}, %PointGroup{id: point_group_id}) do
any_body_node_uses_point_group?(body, point_group_id)
end
def uses_point_group?(%Regimen{body: body, regimen_items: regimen_items}, %PointGroup{
id: point_group_id
}) do
any_body_node_uses_point_group?(body, point_group_id) ||
Enum.find(regimen_items, fn %{sequence_id: sequence_id} ->
any_body_node_uses_point_group?(get_sequence(sequence_id).body, point_group_id)
end)
def uses_point_group?(%Regimen{body: body, regimen_items: regimen_items}, %PointGroup{id: point_group_id}) do
any_body_node_uses_point_group?(body, point_group_id) || Enum.find(regimen_items, fn(%{sequence_id: sequence_id}) ->
any_body_node_uses_point_group?(get_sequence(sequence_id).body, point_group_id)
end)
end
def uses_point_group?(%RegimenInstance{farm_event: farm_event, regimen: regimen}, point_group) do
@ -401,13 +350,11 @@ defmodule FarmbotCore.Asset do
end
def any_body_node_uses_point_group?(body, point_group_id) do
Enum.find(body, fn
Enum.find(body, fn
%{
kind: "execute",
body: execute_body
} ->
any_body_node_uses_point_group?(execute_body, point_group_id)
} -> any_body_node_uses_point_group?(execute_body, point_group_id)
%{
args: %{
"data_value" => %{
@ -417,8 +364,7 @@ defmodule FarmbotCore.Asset do
"label" => "parent"
},
kind: "parameter_application"
} ->
true
} -> true
%{
args: %{
@ -429,11 +375,8 @@ defmodule FarmbotCore.Asset do
"label" => "parent"
},
kind: "parameter_application"
} ->
true
_ ->
false
} -> true
_ -> false
end)
end
@ -464,7 +407,6 @@ defmodule FarmbotCore.Asset do
def new_public_key_from_home!() do
public_key_path = Path.join([System.get_env("HOME"), ".ssh", "id_rsa.pub"])
public_key = File.read!(public_key_path)
%PublicKey{}
|> PublicKey.changeset(%{public_key: public_key})
|> Repo.insert()
@ -475,7 +417,7 @@ defmodule FarmbotCore.Asset do
|> PublicKey.changeset(%{public_key: public_key})
|> Repo.insert()
end
## End PublicKey
## Begin Regimen
@ -493,23 +435,18 @@ defmodule FarmbotCore.Asset do
end
def delete_regimen!(regimen) do
regimen_instances =
Repo.all(from(ri in RegimenInstance, where: ri.regimen_id == ^regimen.local_id))
regimen_instances = Repo.all(from ri in RegimenInstance, where: ri.regimen_id == ^regimen.local_id)
for ri <- regimen_instances do
IO.puts("deleting regimen instance: #{inspect(ri)}")
IO.puts "deleting regimen instance: #{inspect(ri)}"
delete_regimen_instance!(ri)
end
Repo.delete!(regimen)
end
@doc "Update an existing regimen"
def update_regimen!(regimen, params) do
regimen_instances =
Repo.all(from(ri in RegimenInstance, where: ri.regimen_id == ^regimen.local_id))
|> Repo.preload([:farm_event, :regimen])
regimen_instances = Repo.all(from ri in RegimenInstance, where: ri.regimen_id == ^regimen.local_id)
|> Repo.preload([:farm_event, :regimen])
for ri <- regimen_instances do
ri
|> RegimenInstance.changeset(%{updated_at: DateTime.utc_now()})
@ -532,31 +469,23 @@ defmodule FarmbotCore.Asset do
def update_sequence!(%Sequence{} = sequence, params \\ %{}) do
sequence_id = sequence.id
farm_events = Repo.all(from f in FarmEvent,
where: f.executable_type == "Sequence"
and f.executable_id == ^sequence_id)
farm_events =
Repo.all(
from(f in FarmEvent,
where:
f.executable_type == "Sequence" and
f.executable_id == ^sequence_id
)
)
regimen_instances =
RegimenInstance
|> Repo.all()
|> Repo.preload([:regimen, :farm_event])
|> Enum.filter(fn
%{regimen: %{regimen_items: items}} ->
Enum.find(items, fn
%{sequence_id: ^sequence_id} -> true
%{sequence_id: _} -> true
end)
%{regimen: nil} ->
false
regimen_instances = RegimenInstance
|> Repo.all()
|> Repo.preload([:regimen, :farm_event])
|> Enum.filter(fn
%{regimen: %{regimen_items: items}} ->
Enum.find(items, fn
%{sequence_id: ^sequence_id} -> true
%{sequence_id: _} -> true
end)
%{regimen: nil} -> false
end)
for asset <- farm_events ++ regimen_instances do
FarmbotCore.AssetSupervisor.update_child(asset)
end
@ -578,11 +507,10 @@ defmodule FarmbotCore.Asset do
def get_farmware_manifest(package) do
first_party_farmwares = Repo.all(from(fwi in FirstPartyFarmware, select: fwi.manifest))
regular_farmwares = Repo.all(from(fwi in FarmwareInstallation, select: fwi.manifest))
Enum.find(
first_party_farmwares ++ regular_farmwares,
fn
%{package: pkg} -> pkg == package
first_party_farmwares ++ regular_farmwares,
fn
%{package: pkg} -> pkg == package
_ -> false
end
)
@ -591,11 +519,10 @@ defmodule FarmbotCore.Asset do
def get_farmware_installation(package) do
first_party_farmwares = Repo.all(from(fwi in FirstPartyFarmware))
regular_farmwares = Repo.all(from(fwi in FarmwareInstallation))
Enum.find(
first_party_farmwares ++ regular_farmwares,
fn
%{manifest: %{package: pkg}} -> pkg == package
first_party_farmwares ++ regular_farmwares,
fn
%{manifest: %{package: pkg}} -> pkg == package
_ -> false
end
)
@ -603,14 +530,12 @@ defmodule FarmbotCore.Asset do
def upsert_farmware_manifest_by_id(id, params) do
fwi = Repo.get_by(FarmwareInstallation, id: id) || %FarmwareInstallation{}
FarmwareInstallation.changeset(fwi, params)
|> Repo.insert_or_update()
end
def upsert_first_party_farmware_manifest_by_id(id, params) do
fwi = Repo.get_by(FirstPartyFarmware, id: id) || %FirstPartyFarmware{}
FirstPartyFarmware.changeset(fwi, params)
|> Repo.insert_or_update()
end
@ -632,14 +557,12 @@ defmodule FarmbotCore.Asset do
def new_farmware_env(params) do
key = params["key"] || params[:key]
fwe =
with key when is_binary(key) <- key,
[fwe | _] <- Repo.all(from(fwe in FarmwareEnv, where: fwe.key == ^key)) do
fwe
else
_ -> %FarmwareEnv{}
end
fwe = with key when is_binary(key) <- key,
[fwe | _] <- Repo.all(from fwe in FarmwareEnv, where: fwe.key == ^key) do
fwe
else
_ -> %FarmwareEnv{}
end
FarmwareEnv.changeset(fwe, params)
|> Repo.insert_or_update()
@ -673,7 +596,7 @@ defmodule FarmbotCore.Asset do
Sensor.changeset(%Sensor{}, params)
|> Repo.insert!()
end
def update_sensor!(sensor, params) do
sensor
|> Sensor.changeset(params)
@ -701,6 +624,15 @@ defmodule FarmbotCore.Asset do
## End SensorReading
## Begin DiagnosticDump
def new_diagnostic_dump(params) do
DiagnosticDump.changeset(%DiagnosticDump{}, params)
|> Repo.insert()
end
## End DiagnosticDump
## Begin DeviceCert
def new_device_cert(params) do

View File

@ -4,7 +4,6 @@ defmodule FarmbotCore.Asset.Command do
"""
require Logger
alias FarmbotCore.{Asset, Asset.Repo}
alias FarmbotCore.Asset.{
Device,
FarmEvent,
@ -45,31 +44,31 @@ defmodule FarmbotCore.Asset.Command do
:ok
end
def update(Device, _id, params) do
def update(Device, _id, params) do
Asset.update_device!(params)
:ok
end
def update(FbosConfig, id, nil) do
def update(FbosConfig, id, nil) do
Asset.delete_fbos_config!(id)
:ok
end
def update(FbosConfig, _id, params) do
def update(FbosConfig, _id, params) do
Asset.update_fbos_config!(params)
:ok
end
def update(FirmwareConfig, id, nil) do
def update(FirmwareConfig, id, nil) do
Asset.delete_firmware_config!(id)
:ok
end
def update(FirmwareConfig, _id, params) do
def update(FirmwareConfig, _id, params) do
Asset.update_firmware_config!(params)
:ok
end
# Deletion use case:
# TODO(Connor) put checks for deleting Device, FbosConfig and FirmwareConfig
@ -97,12 +96,12 @@ defmodule FarmbotCore.Asset.Command do
:ok
end
def update(FarmwareEnv, id, params) do
def update(FarmwareEnv, id, params) do
Asset.upsert_farmware_env_by_id(id, params)
:ok
end
def update(FarmwareInstallation, id, params) do
def update(FarmwareInstallation, id, params) do
Asset.upsert_farmware_manifest_by_id(id, params)
:ok
end
@ -113,17 +112,15 @@ defmodule FarmbotCore.Asset.Command do
def update(FarmEvent, id, params) do
old = Asset.get_farm_event(id)
if old,
do: Asset.update_farm_event!(old, params),
if old,
do: Asset.update_farm_event!(old, params),
else: Asset.new_farm_event!(params)
:ok
end
def update(PublicKey, id, params) do
old = Asset.get_public_key(id)
if old,
do: Asset.update_public_key!(old, params),
else: Asset.new_public_key!(params)
@ -133,37 +130,33 @@ defmodule FarmbotCore.Asset.Command do
def update(Regimen, id, params) do
old = Asset.get_regimen(id)
if old,
do: Asset.update_regimen!(old, params),
if old,
do: Asset.update_regimen!(old, params),
else: Asset.new_regimen!(params)
:ok
end
def update(Sensor, id, params) do
old = Asset.get_sensor(id)
if old,
do: Asset.update_sensor!(old, params),
if old,
do: Asset.update_sensor!(old, params),
else: Asset.new_sensor!(params)
:ok
end
def update(SensorReading, id, params) do
old = Asset.get_sensor_reading(id)
if old,
do: Asset.update_sensor_reading!(old, params),
if old,
do: Asset.update_sensor_reading!(old, params),
else: Asset.new_sensor_reading!(params)
:ok
end
def update(Sequence, id, params) do
old = Asset.get_sequence(id)
if old,
do: Asset.update_sequence!(old, params),
else: Asset.new_sequence!(params)
@ -179,7 +172,6 @@ defmodule FarmbotCore.Asset.Command do
def update(PointGroup, id, params) do
old = Asset.get_point_group(id: id)
if old,
do: Asset.update_point_group!(old, params),
else: Asset.new_point_group!(params)
@ -189,8 +181,8 @@ defmodule FarmbotCore.Asset.Command do
# Catch-all use case:
def update(asset_kind, id, params) do
Logger.warn "AssetCommand needs implementation: #{asset_kind}"
mod = as_module!(asset_kind)
case Repo.get_by(mod, id: id) do
nil ->
struct!(mod)
@ -213,27 +205,28 @@ defmodule FarmbotCore.Asset.Command do
mod.changeset(asset, params)
end
defp as_module!("Device"), do: Asset.Device
defp as_module!("FarmEvent"), do: Asset.FarmEvent
defp as_module!("FarmwareEnv"), do: Asset.FarmwareEnv
defp as_module!("FirstPartyFarmware"), do: Asset.FirstPartyFarmware
defp as_module!("FarmwareInstallation"), do: Asset.FarmwareInstallation
defp as_module!("FbosConfig"), do: Asset.FbosConfig
defp as_module!("FirmwareConfig"), do: Asset.FirmwareConfig
defp as_module!("Peripheral"), do: Asset.Peripheral
defp as_module!("PinBinding"), do: Asset.PinBinding
defp as_module!("Point"), do: Asset.Point
defp as_module!("PointGroup"), do: Asset.PointGroup
defp as_module!("Regimen"), do: Asset.Regimen
defp as_module!("Sensor"), do: Asset.Sensor
defp as_module!("SensorReading"), do: Asset.SensorReading
defp as_module!("Sequence"), do: Asset.Sequence
defp as_module!("Tool"), do: Asset.Tool
defp as_module!("Device"), do: Asset.Device
defp as_module!("DiagnosticDump"), do: Asset.DiagnosticDump
defp as_module!("FarmEvent"), do: Asset.FarmEvent
defp as_module!("FarmwareEnv"), do: Asset.FarmwareEnv
defp as_module!("FirstPartyFarmware"), do: Asset.FirstPartyFarmware
defp as_module!("FarmwareInstallation"), do: Asset.FarmwareInstallation
defp as_module!("FbosConfig"), do: Asset.FbosConfig
defp as_module!("FirmwareConfig"), do: Asset.FirmwareConfig
defp as_module!("Peripheral"), do: Asset.Peripheral
defp as_module!("PinBinding"), do: Asset.PinBinding
defp as_module!("Point"), do: Asset.Point
defp as_module!("PointGroup"), do: Asset.PointGroup
defp as_module!("Regimen"), do: Asset.Regimen
defp as_module!("Sensor"), do: Asset.Sensor
defp as_module!("SensorReading"), do: Asset.SensorReading
defp as_module!("Sequence"), do: Asset.Sequence
defp as_module!("Tool"), do: Asset.Tool
defp as_module!(module) when is_atom(module) do
as_module!(List.last(Module.split(module)))
end
defp as_module!(kind) when is_binary(kind) do
raise("""
Unknown kind: #{kind}

View File

@ -1,183 +0,0 @@
defmodule FarmbotCore.Asset.CriteriaRetriever do
alias FarmbotCore.Asset.{ PointGroup, Repo, Point }
import Ecto.Query
@moduledoc """
The PointGroup asset declares a list
of criteria to query points. This
module then converts that criteria to
a list of real points that match the
criteria of a point group.
Example:
You have a PointGroup with a criteria
where group.criteria.number_gt.x == 10
Passing that PointGroup to this module
will return an array of `Point` assets
with an x property that is greater than
10.
"""
# We will not query any string/numeric fields other than these.
# Updating the PointGroup / Point models may require an update
# to these fields.
@numberic_fields ["radius", "x", "y", "z", "pullout_direction"]
@string_fields ["name", "openfarm_slug", "plant_stage", "pointer_type"]
@doc """
You provide it a %PointGroup{},
it provides you an array of
point records (%Point{}) that match
the group's criteria.
"""
def run(%PointGroup{point_ids: static_ids} = pg) do
# pg.point_ids is *always* include in search results,
# even if it does not match pg.criteria in any way.
always_ok = Repo.all(from(p in Point, where: p.id in ^static_ids, select: p))
# Now we need a list of point IDs that actually match
# the pg.criteria fields. We only get the ID because
# we are circumventing Ecto and doing raw SQL.
# There may be better ways to do this:
dynamic_ids = find_matching_point_ids(pg)
# Once we have a list of matching criteria, we can run a
# SQL query through Ecto to return real %Point{} structs...
dynamic_query = from(p in Point, where: p.id in ^dynamic_ids, select: p)
# ...but we're not done! If the criteria contains meta fields,
# we need to perform a lookup in memory
needs_meta_filter = Repo.all(dynamic_query)
# There we go. We have all the matching %Point{}s
search_matches = search_meta_fields(pg, needs_meta_filter)
# ...but there are duplicates. We can remove them via uniq_by:
Enum.uniq_by((search_matches ++ always_ok), fn p -> p.id end)
end
@doc """
Takes intermediate search results and makes them
more specific by iterating over the search results
and only returning the ones that match the meta.*
field provided in pg.criteria
"""
def search_meta_fields(%PointGroup{} = pg, points) do
meta = "meta."
meta_len = String.length(meta)
(pg.criteria["string_eq"] || %{})
|> Map.to_list()
|> Enum.filter(fn {k, _v} ->
String.starts_with?(k, meta)
end)
|> Enum.map(fn {k, value} ->
clean_key = String.slice(k, ((meta_len)..-1))
{clean_key, value}
end)
|> Enum.reduce(%{}, fn {key, value}, all ->
all_values = all[key] || []
Map.merge(all, %{key => value ++ all_values})
end)
|> Map.to_list()
|> Enum.reduce(points, fn {key, values}, finalists ->
finalists
|> Enum.filter(fn point -> point.meta[key] end)
|> Enum.filter(fn point -> point.meta[key] in values end)
end)
end
# Find all point IDs that are matched by a PointGroup
# This is not including any meta.* search terms,
# since `meta` is a JSON column that must be
# manually searched in memory.
defp find_matching_point_ids(%PointGroup{} = pg) do
results = {pg, []}
|> stage_1("string_eq", @string_fields, "IN")
|> stage_1("number_eq", @numberic_fields, "IN")
|> stage_1("number_gt", @numberic_fields, ">")
|> stage_1("number_lt", @numberic_fields, "<")
|> stage_1_day_field()
|> unwrap_stage_1()
|> Enum.reduce(%{}, &stage_2/2)
|> Map.to_list()
|> Enum.reduce({[], [], 0}, &stage_3/2)
|> unwrap_stage_3()
|> finalize()
results
end
# EDGE CASE: If the user _only_ wants to put static points
# in their group, we need to perform 0 SQL queries.
def finalize({_, []}) do
[]
end
def finalize({fragments, criteria}) do
x = Enum.join(fragments, " AND ")
sql = "SELECT id FROM points WHERE #{x}"
query_params = List.flatten(criteria)
{:ok, query} = Repo.query(sql, query_params)
%Sqlite.DbConnection.Result{ rows: rows } = query
List.flatten(rows)
end
defp unwrap_stage_1({_, right}), do: right
defp unwrap_stage_3({query, args, _count}), do: {query, args}
defp stage_1({pg, accum}, kind, fields, op) do
results = fields
|> Enum.map(fn field -> {field, pg.criteria[kind][field]} end)
|> Enum.filter(fn {_k, v} -> v end)
|> Enum.map(fn {k, v} -> {k, op, v} end)
{pg, accum ++ results}
end
defp stage_1_day_field({pg, accum}) do
day_criteria = pg.criteria["day"] || pg.criteria[:day] || %{}
days = day_criteria["days_ago"] || day_criteria[:days_ago] || 0
op = day_criteria["op"] || day_criteria[:op] || "<"
time = Timex.shift(Timex.now(), days: -1 * days)
if days == 0 do
{ pg, accum }
else
inverted_op = if op == ">" do "<" else ">" end
{ pg, accum ++ [{"created_at", inverted_op, time}] }
end
end
defp stage_2({lhs, "IN", rhs}, results) do
query = "#{lhs} IN"
all_values = results[query] || []
Map.merge(results, %{query => rhs ++ all_values})
end
defp stage_2({lhs, op, rhs}, results) do
Map.merge(results, %{"#{lhs} #{op}" => rhs})
end
# Interpolatinng data turned out to be hard.
# Pretty sure there is an easier way to do this.
# NOT OK: Repo.query("SELECT foo WHERE bar IN $0", [[1, 2, 3]])
# OK: Repo.query("SELECT foo WHERE bar IN ($0, $1, $2)", [1, 2, 3])
defp stage_3({sql, args}, {full_query, full_args, count0}) when is_list(args) do
arg_count = Enum.count(args)
final = count0 + (arg_count - 1)
initial_state = {sql, count0}
{next_sql, _} =
if arg_count == 1 do
{sql <> " ($#{count0})", nil}
else
Enum.reduce(args, initial_state, fn
(_, {sql, ^count0}) -> {sql <> " ($#{count0},", count0+1}
(_, {sql, ^final}) -> {sql <> " $#{final})", final}
(_, {sql, count}) -> {sql <> " $#{count},", count+1}
end)
end
{full_query ++ [next_sql], full_args ++ [args], final + 1}
end
defp stage_3({sql, args}, {full_query, full_args, count}) do
{full_query ++ [sql <> " $#{count}"], full_args ++ [args], count + 1}
end
end

View File

@ -0,0 +1,58 @@
defmodule FarmbotCore.Asset.DiagnosticDump do
@moduledoc """
Just the DiagDump REST resource, used by FarmBot staff to help users debug
remote device problems.
"""
use FarmbotCore.Asset.Schema, path: "/api/diagnostic_dumps"
schema "diagnostic_dumps" do
field(:id, :id)
has_one(:local_meta, FarmbotCore.Asset.Private.LocalMeta,
on_delete: :delete_all,
references: :local_id,
foreign_key: :asset_local_id
)
field(:ticket_identifier, :string)
field(:fbos_commit, :string)
field(:fbos_version, :string)
field(:firmware_commit, :string)
field(:firmware_state, :string)
field(:network_interface, :string)
field(:fbos_dmesg_dump, :string)
field(:monitor, :boolean, default: true)
timestamps()
end
view diagnostic_dump do
%{
id: diagnostic_dump.id,
ticket_identifier: diagnostic_dump.ticket_identifier,
fbos_commit: diagnostic_dump.fbos_commit,
fbos_version: diagnostic_dump.fbos_version,
firmware_commit: diagnostic_dump.firmware_commit,
firmware_state: diagnostic_dump.firmware_state,
network_interface: diagnostic_dump.network_interface,
fbos_dmesg_dump: diagnostic_dump.fbos_dmesg_dump
}
end
def changeset(diagnostic_dump, params \\ %{}) do
diagnostic_dump
|> cast(params, [
:id,
:ticket_identifier,
:fbos_commit,
:fbos_version,
:firmware_commit,
:firmware_state,
:network_interface,
:fbos_dmesg_dump,
:monitor,
:created_at,
:updated_at
])
|> validate_required([])
end
end

View File

@ -42,6 +42,7 @@ defmodule FarmbotCore.Asset.FbosConfig do
field(:sequence_body_log, :boolean)
field(:sequence_complete_log, :boolean)
field(:sequence_init_log, :boolean)
field(:boot_sequence_id, :id)
# private
field(:monitor, :boolean, default: true)
@ -65,7 +66,8 @@ defmodule FarmbotCore.Asset.FbosConfig do
update_channel: fbos_config.update_channel,
sequence_body_log: fbos_config.sequence_body_log,
sequence_complete_log: fbos_config.sequence_complete_log,
sequence_init_log: fbos_config.sequence_init_log
sequence_init_log: fbos_config.sequence_init_log,
boot_sequence_id: fbos_config.boot_sequence_id
}
end
@ -88,6 +90,7 @@ defmodule FarmbotCore.Asset.FbosConfig do
:sequence_body_log,
:sequence_complete_log,
:sequence_init_log,
:boot_sequence_id,
:monitor,
:created_at,
:updated_at

View File

@ -100,15 +100,16 @@ defimpl String.Chars, for: FarmbotCore.Asset.PinBinding do
end
defp special_action(button_number, action, pin_num) do
"Button #{button_number}: #{format_action(action)} (Pi #{pin_num})"
"Button #{button_number}: #{format_action(action)} (Pi #{pin_num})"
end
defp format_action("emergency_lock"), do: "E-Stop"
defp format_action("dump_info"), do: "Dump Info"
defp format_action("emergency_lock"), do: "E-Stop"
defp format_action("emergency_unlock"), do: "E-Unlock"
defp format_action("power_off"), do: "Power Off"
defp format_action("read_status"), do: "Read Status"
defp format_action("reboot"), do: "Reboot"
defp format_action("sync"), do: "Sync"
defp format_action("take_photo"), do: "Take Photo"
defp format_action("take_photo"), do: "Take Photo"
defp format_action(_), do: nil
end

View File

@ -13,21 +13,18 @@ defmodule FarmbotCore.Asset.Point do
foreign_key: :asset_local_id
)
field(:discarded_at, :utc_datetime)
field(:gantry_mounted, :boolean)
field(:meta, :map)
field(:monitor, :boolean, default: true)
field(:name, :string)
field(:openfarm_slug, :string)
field(:plant_stage, :string)
field(:planted_at, :utc_datetime)
field(:pointer_type, :string)
field(:pullout_direction, :integer)
field(:radius, :float)
field(:tool_id, :integer)
field(:x, :float)
field(:y, :float)
field(:z, :float)
field(:tool_id, :integer)
field(:discarded_at, :utc_datetime)
field(:monitor, :boolean, default: true)
timestamps()
end
@ -37,15 +34,11 @@ defmodule FarmbotCore.Asset.Point do
meta: point.meta,
name: point.name,
plant_stage: point.plant_stage,
created_at: point.created_at,
planted_at: point.planted_at,
pointer_type: point.pointer_type,
radius: point.radius,
tool_id: point.tool_id,
discarded_at: point.discarded_at,
gantry_mounted: point.gantry_mounted,
openfarm_slug: point.openfarm_slug,
pullout_direction: point.pullout_direction,
x: point.x,
y: point.y,
z: point.z
@ -55,24 +48,21 @@ defmodule FarmbotCore.Asset.Point do
def changeset(point, params \\ %{}) do
point
|> cast(params, [
:created_at,
:discarded_at,
:gantry_mounted,
:id,
:meta,
:monitor,
:name,
:plant_stage,
:planted_at,
:pointer_type,
:pullout_direction,
:openfarm_slug,
:radius,
:tool_id,
:updated_at,
:x,
:y,
:z,
:tool_id,
:discarded_at,
:monitor,
:created_at,
:updated_at
])
|> validate_required([])
end

View File

@ -5,18 +5,6 @@ defmodule FarmbotCore.Asset.PointGroup do
use FarmbotCore.Asset.Schema, path: "/api/point_groups"
@default_criteria %{
"day" => %{ "op" => ">", "days_ago" => 0 },
# Map<string, string[] | undefined>,
"string_eq" => %{},
# Map<string, number[] | undefined>,
"number_eq" => %{},
# Map<string, number | undefined>,
"number_lt" => %{},
# Map<string, number | undefined>,
"number_gt" => %{},
}
schema "point_groups" do
field(:id, :id)
@ -29,7 +17,6 @@ defmodule FarmbotCore.Asset.PointGroup do
field(:name, :string)
field(:point_ids, {:array, :integer})
field(:sort_type, :string)
field(:criteria, :map, default: @default_criteria)
field(:monitor, :boolean, default: true)
timestamps()
@ -40,23 +27,13 @@ defmodule FarmbotCore.Asset.PointGroup do
id: point_group.id,
name: point_group.name,
point_ids: point_group.point_ids,
sort_type: point_group.sort_type,
criteria: point_group.criteria
sort_type: point_group.sort_type
}
end
def changeset(point_group, params \\ %{}) do
point_group
|> cast(params, [
:id,
:name,
:criteria,
:point_ids,
:sort_type,
:monitor,
:created_at,
:updated_at
])
|> cast(params, [:id, :name, :point_ids, :sort_type, :monitor, :created_at, :updated_at])
|> validate_required([])
end
end

View File

@ -40,17 +40,17 @@ defmodule FarmbotCore.Asset.Private do
# Because sqlite can't test unique constraints before a transaction, if this function gets called for
# the same asset more than once asyncronously, the asset can be marked dirty twice at the same time
# causing the `unique constraint` error to happen in either `ecto` OR `sqlite`. I've
# caught both errors here as they are both essentially the same thing, and can be safely
# caught both errors here as they are both essentially the same thing, and can be safely
# discarded. Doing an `insert_or_update/1` (without the bang) can still result in the sqlite
# error being thrown.
# error being thrown.
changeset = LocalMeta.changeset(local_meta, Map.merge(params, %{table: table, status: "dirty"}))
try do
Repo.insert_or_update!(changeset)
catch
:error, %Sqlite.DbConnection.Error{
message: "UNIQUE constraint failed: local_metas.table, local_metas.asset_local_id",
message: "UNIQUE constraint failed: local_metas.table, local_metas.asset_local_id",
sqlite: %{code: :constraint}
} ->
} ->
Logger.warn """
Caught race condition marking data as dirty (sqlite)
table: #{inspect(table)}
@ -59,10 +59,10 @@ defmodule FarmbotCore.Asset.Private do
Ecto.Changeset.apply_changes(changeset)
:error, %Ecto.InvalidChangesetError{
changeset: %{
action: :insert,
action: :insert,
errors: [
table: {"LocalMeta already exists.", [
validation: :unsafe_unique,
validation: :unsafe_unique,
fields: [:table, :asset_local_id]
]}
]}
@ -73,7 +73,7 @@ defmodule FarmbotCore.Asset.Private do
id: #{inspect(asset.local_id)}
"""
Ecto.Changeset.apply_changes(changeset)
type, reason ->
type, reason ->
FarmbotCore.Logger.error 1, """
Caught unexpected error marking data as dirty
table: #{inspect(table)}

View File

@ -9,6 +9,7 @@ defmodule FarmbotCore.Asset.Private.LocalMeta do
alias FarmbotCore.Asset.{
Repo,
Device,
DiagnosticDump,
DeviceCert,
FarmEvent,
FarmwareEnv,
@ -46,6 +47,13 @@ defmodule FarmbotCore.Asset.Private.LocalMeta do
define_field: false
)
belongs_to(:diagnostic_dump, DiagnosticDump,
foreign_key: :asset_local_id,
type: :binary_id,
references: :local_id,
define_field: false
)
belongs_to(:farm_event, FarmEvent,
foreign_key: :asset_local_id,
type: :binary_id,
@ -166,7 +174,8 @@ defmodule FarmbotCore.Asset.Private.LocalMeta do
"firmware_configs",
"fbos_configs",
"farmware_installations",
"farmware_envs"
"farmware_envs",
"diagnostic_dumps"
])
|> unsafe_validate_unique([:table, :asset_local_id], Repo,
message: "LocalMeta already exists."

View File

@ -7,8 +7,15 @@ defmodule FarmbotCore.Asset.Query do
@callback auto_sync?() :: boolean()
@callback first_sync?() :: boolean()
@doc "Returns the configuration value for auto_sync"
def auto_sync?() do
Asset.fbos_config().auto_sync
end
@doc "Checks if initial syncing is still required"
def first_sync?() do
is_nil(Asset.fbos_config().id)
end
end

View File

@ -23,11 +23,7 @@ defmodule FarmbotCore.Asset.Supervisor do
end
def init([]) do
Supervisor.init(children(), strategy: :one_for_one)
end
def children do
default = [
children = [
Repo,
{AssetSupervisor, module: FbosConfig},
{AssetSupervisor, module: FirmwareConfig},
@ -42,7 +38,7 @@ defmodule FarmbotCore.Asset.Supervisor do
{AssetSupervisor, module: FarmwareEnv},
AssetMonitor,
]
config = Application.get_env(:farmbot_ext, __MODULE__) || []
Keyword.get(config, :children, default)
Supervisor.init(children, strategy: :one_for_one)
end
end

View File

@ -41,6 +41,7 @@ defmodule FarmbotCore.Asset.Sync do
embeds_many(:devices, Item)
embeds_many(:firmware_configs, Item)
embeds_many(:fbos_configs, Item)
embeds_many(:diagnostic_dumps, Item)
embeds_many(:farm_events, Item)
embeds_many(:farmware_envs, Item)
embeds_many(:first_party_farmwares, Item)
@ -64,6 +65,7 @@ defmodule FarmbotCore.Asset.Sync do
devices: Enum.map(sync.device, &Item.render/1),
fbos_configs: Enum.map(sync.fbos_config, &Item.render/1),
firmware_configs: Enum.map(sync.firmware_config, &Item.render/1),
diagnostic_dumps: Enum.map(sync.diagnostic_dumps, &Item.render/1),
farm_events: Enum.map(sync.farm_events, &Item.render/1),
farmware_envs: Enum.map(sync.farmware_envs, &Item.render/1),
first_party_farmwares: Enum.map(sync.first_party_farmwares, &Item.render/1),
@ -88,6 +90,7 @@ defmodule FarmbotCore.Asset.Sync do
|> cast_embed(:devices)
|> cast_embed(:fbos_configs)
|> cast_embed(:firmware_configs)
|> cast_embed(:diagnostic_dumps)
|> cast_embed(:farm_events)
|> cast_embed(:farmware_envs)
|> cast_embed(:farmware_installations)

View File

@ -1,26 +1,25 @@
defmodule FarmbotCore.AssetHelpers do
@moduledoc """
Helpers for the console at runtime.
Example:
iex()> use FarmbotCore.AssetHelpers
iex()> Repo.all(Device)
[%Device{}]
"""
@doc false
defmacro __using__(_opts) do
require Logger
Logger.warn("Don't use this in production please!")
Logger.warn "Don't use this in production please!"
quote do
import Ecto.Query
alias FarmbotCore.Asset
alias Asset.{
Repo,
Device,
DeviceCert,
DiagnosticDump,
FarmwareEnv,
FarmwareInstallation,
FirstPartyFarmware,
@ -41,4 +40,4 @@ defmodule FarmbotCore.AssetHelpers do
}
end
end
end
end

View File

@ -82,6 +82,7 @@ defmodule FarmbotCore.AssetMonitor do
sub_state = Map.drop(sub_state, deleted_ids)
Enum.each(deleted_ids, fn local_id ->
Logger.error("#{inspect(kind)} #{local_id} needs to be terminated")
AssetSupervisor.terminate_child(kind, local_id)
end)
@ -93,11 +94,13 @@ defmodule FarmbotCore.AssetMonitor do
Map.put(sub_state, id, updated_at)
is_nil(sub_state[id]) ->
Logger.debug("#{inspect(kind)} #{id} needs to be started")
asset = Repo.preload(asset, AssetWorker.preload(asset))
:ok = AssetSupervisor.start_child(asset) |> assert_result!(asset)
Map.put(sub_state, id, updated_at)
compare_datetimes(updated_at, sub_state[id]) == :gt ->
Logger.warn("#{inspect(kind)} #{id} needs to be updated")
asset = Repo.preload(asset, AssetWorker.preload(asset))
:ok = AssetSupervisor.update_child(asset) |> assert_result!(asset)
Map.put(sub_state, id, updated_at)

View File

@ -6,6 +6,20 @@ defmodule FarmbotCore.AssetSupervisor do
use Supervisor
alias FarmbotCore.{Asset.Repo, AssetWorker}
def get_state(%{} = asset) do
case whereis_child(asset) do
{_id, pid, _, _} -> :sys.get_state(pid)
_ -> :error
end
end
def get_pid(%{} = asset) do
case whereis_child(asset) do
{_id, pid, _, _} -> pid
_ -> :error
end
end
@doc "List all children for an asset"
def list_children(kind) do
name = Module.concat(__MODULE__, kind)

View File

@ -66,9 +66,9 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.Device do
{:mounted_tool_id, nil} ->
if old_device.mounted_tool_id do
if tool = Asset.get_tool(id: old_device.mounted_tool_id) do
FarmbotCore.Logger.info(2, "Dismounted the #{tool.name}")
FarmbotCore.Logger.info(2, "Farmbot dismounted #{tool.name}")
else
FarmbotCore.Logger.info(2, "Dismounted unknown tool")
FarmbotCore.Logger.info(2, "Farmbot dismounted unknown tool")
end
else
# no previously mounted tool
@ -77,9 +77,9 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.Device do
{:mounted_tool_id, id} ->
if tool = Asset.get_tool(id: id) do
FarmbotCore.Logger.info(2, "Mounted the #{tool.name}")
FarmbotCore.Logger.info(2, "Farmbot mounted #{tool.name}")
else
FarmbotCore.Logger.info(2, "Mounted unknown tool")
FarmbotCore.Logger.info(2, "Farmbot mounted unknown tool")
end
{_key, _value} ->

View File

@ -2,7 +2,7 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.FarmwareInstallation do
use GenServer
require FarmbotCore.Logger
alias FarmbotCore.{Asset.Repo, BotState, JSON}
alias FarmbotCore.{Asset.Repo, BotState, DepTracker, JSON}
alias FarmbotCore.Asset.FarmwareInstallation, as: FWI
alias FarmbotCore.Asset.FarmwareInstallation.Manifest
@ -21,7 +21,9 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.FarmwareInstallation do
end
def init(fwi) do
{:ok, %{fwi: fwi, backoff: 0}, 0}
:ok = DepTracker.register_asset(fwi, :init)
send self(), :timeout
{:ok, %{fwi: fwi, backoff: 0}}
end
def handle_cast(:update, state) do
@ -39,6 +41,7 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.FarmwareInstallation do
:ok <- install_zip(updated, zip_binary),
:ok <- install_farmware_tools(updated),
:ok <- write_manifest(updated) do
:ok = DepTracker.register_asset(fwi, :complete)
FarmbotCore.Logger.success(1, "Installed Farmware: #{updated.manifest.package}")
# TODO(Connor) -> No reason to keep this process alive?
BotState.report_farmware_installed(updated.manifest.package, Manifest.view(updated.manifest))
@ -47,8 +50,9 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.FarmwareInstallation do
error ->
backoff = state.backoff + @back_off_time_ms
timeout = @error_retry_time_ms + backoff
Process.send_after(self(), :timeout, timeout)
error_log(fwi, "Failed to download Farmware manifest. Trying again in #{timeout}ms #{inspect(error)}")
{:noreply, %{state | backoff: backoff}, timeout}
{:noreply, %{state | backoff: backoff}}
end
end
@ -76,14 +80,17 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.FarmwareInstallation do
updated =
FWI.changeset(fwi, %{manifest: nil, updated_at: fwi.updated_at})
|> Repo.update!()
{:noreply, %{state | fwi: updated}, 0}
send self(), :timeout
{:noreply, %{state | fwi: updated}}
error ->
:ok = DepTracker.register_asset(fwi, :complete)
BotState.report_farmware_installed(fwi.manifest.package, Manifest.view(fwi.manifest))
backoff = state.backoff + @back_off_time_ms
timeout = @error_retry_time_ms + backoff
Process.send_after(self(), :timeout, timeout)
error_log(fwi, "failed to check for updates. Trying again in #{timeout}ms #{inspect(error)}")
{:noreply, %{state | backoff: backoff}, timeout}
{:noreply, %{state | backoff: backoff}}
end
end
@ -92,6 +99,7 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.FarmwareInstallation do
# Installed is newer than remote.
:gt ->
success_log(updated, "up to date.")
:ok = DepTracker.register_asset(updated, :complete)
BotState.report_farmware_installed(updated.manifest.package, Manifest.view(updated.manifest))
{:noreply, %{state | fwi: updated}}
@ -99,6 +107,7 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.FarmwareInstallation do
# No difference between installed and remote.
:eq ->
success_log(updated, "up to date.")
:ok = DepTracker.register_asset(updated, :complete)
BotState.report_farmware_installed(updated.manifest.package, Manifest.view(updated.manifest))
{:noreply, %{state | fwi: updated}}
@ -111,6 +120,7 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.FarmwareInstallation do
:ok <- install_zip(updated, zip_binary),
:ok <- install_farmware_tools(updated),
:ok <- write_manifest(updated) do
:ok = DepTracker.register_asset(updated, :complete)
BotState.report_farmware_installed(updated.manifest.package, Manifest.view(updated.manifest))
{:noreply, %{state | fwi: updated, backoff: 0}}
@ -118,8 +128,9 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.FarmwareInstallation do
er ->
backoff = state.backoff + @back_off_time_ms
timeout = @error_retry_time_ms + backoff
Process.send_after(self(), :timeout, timeout)
error_log(updated, "update failed. Trying again in #{timeout}ms #{inspect(er)}")
{:noreply, %{state | fwi: updated, backoff: backoff}, timeout}
{:noreply, %{state | fwi: updated, backoff: backoff}}
end
end
end
@ -246,7 +257,16 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.FarmwareInstallation do
end
defp httpc_options, do: [body_format: :binary]
defp httpc_headers, do: [{'user-agent', 'farmbot-os'}]
case System.get_env("GITHUB_TOKEN") do
nil ->
defp httpc_headers, do: [{'user-agent', 'farmbot-farmware-installer'}]
token when is_binary(token) ->
@token token
require Logger; Logger.info "using github token: #{@token}"
defp httpc_headers, do: [{'user-agent', 'farmbot-farmware-installer'}, {'Authorization', 'token #{@token}'}]
end
def install_dir(%FWI{} = fwi) do
install_dir(fwi.manifest)

View File

@ -8,12 +8,19 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.FbosConfig do
require Logger
require FarmbotCore.Logger
alias FarmbotCeleryScript.AST
alias FarmbotCore.{Asset.FbosConfig, BotState, Config}
alias FarmbotCore.{Asset, Asset.FbosConfig, BotState, Config, DepTracker}
import FarmbotFirmware.PackageUtils, only: [package_to_string: 1]
@firmware_flash_attempt_threshold Application.get_env(:farmbot_core, __MODULE__)[:firmware_flash_attempt_threshold] || 5
@firmware_flash_attempt_threshold Application.get_env(:farmbot_core, __MODULE__)[:firmware_flash_attempt_threshold]
@firmware_flash_timeout Application.get_env(:farmbot_core, __MODULE__)[:firmware_flash_timeout] || 5000
@disable_firmware_io_logs_timeout Application.get_env(:farmbot_core, __MODULE__)[:disable_firmware_io_logs_timeout] || 300000
@firmware_flash_attempt_threshold || Mix.raise """
Firmware open attempt threshold not configured:
config :farmbot_core, #{__MODULE__}, [
firmware_flash_attempt_threshold: :infinity
]
"""
@impl FarmbotCore.AssetWorker
def preload(%FbosConfig{}), do: []
@ -28,6 +35,14 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.FbosConfig do
@impl GenServer
def init(%FbosConfig{} = fbos_config) do
:ok = DepTracker.register_asset(fbos_config, :init)
%{
informational_settings: %{
idle: fw_idle,
firmware_version: fw_version,
firmware_configured: fw_configured
}
} = BotState.subscribe()
if Config.get_config_value(:bool, "settings", "firmware_needs_flash") do
Config.update_config_value(:bool, "settings", "firmware_needs_open", false)
end
@ -37,20 +52,26 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.FbosConfig do
firmware_flash_attempts: 0,
firmware_flash_attempt_threshold: @firmware_flash_attempt_threshold,
firmware_flash_timeout: @firmware_flash_timeout,
firmware_flash_in_progress: false
firmware_flash_in_progress: false,
firmware_flash_ref: nil,
firmware_idle: fw_idle,
firmware_version: fw_version,
firmware_configured: fw_configured,
}
{:ok, state, 0}
end
@impl GenServer
def handle_info({:step_complete, _, :ok}, state) do
def handle_info({:step_complete, ref, :ok}, %{firmware_flash_ref: ref} = state) do
DepTracker.register_asset(state.fbos_config, :idle)
Config.update_config_value(:bool, "settings", "firmware_needs_flash", false)
Config.update_config_value(:bool, "settings", "firmware_needs_open", true)
{:noreply, %{state | firmware_flash_in_progress: false}}
end
def handle_info({:step_complete, _, {:error, reason}}, %{firmware_flash_attempts: tries, firmware_flash_attempt_threshold: thresh} = state)
def handle_info({:step_complete, ref, {:error, reason}}, %{firmware_flash_ref: ref, firmware_flash_attempts: tries, firmware_flash_attempt_threshold: thresh} = state)
when tries >= thresh do
DepTracker.register_asset(state.fbos_config, :idle)
FarmbotCore.Logger.error 1, """
Failed flashing firmware: #{reason}
Tried #{tries} times. Not retrying
@ -60,7 +81,8 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.FbosConfig do
{:noreply, %{state | firmware_flash_attempts: 0, firmware_flash_in_progress: false}}
end
def handle_info({:step_complete, _, {:error, reason}}, %{fbos_config: %FbosConfig{} = fbos_config} = state) do
def handle_info({:step_complete, ref, {:error, reason}}, %{firmware_flash_ref: ref, fbos_config: %FbosConfig{} = fbos_config} = state) do
DepTracker.register_asset(fbos_config, :flash_firmware)
Config.update_config_value(:bool, "settings", "firmware_needs_flash", true)
Config.update_config_value(:bool, "settings", "firmware_needs_open", false)
@ -90,10 +112,12 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.FbosConfig do
end
def handle_info({:maybe_flash_firmware, old_fbos_config}, %{fbos_config: %FbosConfig{} = fbos_config} = state) do
unless state.firmware_flash_in_progress do
_ = maybe_flash_firmware(state, fbos_config.firmware_hardware, old_fbos_config.firmware_hardware)
if state.firmware_flash_in_progress do
{:noreply, state}
else
ref = maybe_flash_firmware(state, fbos_config.firmware_hardware, old_fbos_config.firmware_hardware)
{:noreply, %{state | firmware_flash_ref: ref}}
end
{:noreply, state}
end
def handle_info({:maybe_start_io_log_timer, old_fbos_config}, %{fbos_config: fbos_config, firmware_io_timer: nil} = state) do
@ -131,6 +155,27 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.FbosConfig do
{:noreply, %{state | fbos_config: new_fbos_config, firmware_io_timer: nil}}
end
def handle_info({BotState, %{changes: %{informational_settings: %{changes: %{idle: idle}}}}}, state) do
{:noreply, %{state | firmware_idle: idle}}
end
def handle_info({BotState, %{changes: %{informational_settings: %{changes: %{firmware_version: fw_version}}}}}, state) do
{:noreply, %{state | firmware_version: fw_version}}
end
def handle_info({BotState, %{changes: %{informational_settings: %{changes: %{firmware_configured: fw_configured}}}}}, state) do
# this should really be fixed upstream not to dispatch if version is none.
if state.firmware_version == "none" do
{:noreply, state}
else
{:noreply, %{state | firmware_configured: fw_configured}}
end
end
def handle_info({BotState, _}, state) do
{:noreply, state}
end
@impl GenServer
def handle_cast({:new_data, new_fbos_config}, %{fbos_config: %FbosConfig{} = old_fbos_config} = state) do
_ = set_config_to_state(new_fbos_config, old_fbos_config)
@ -142,33 +187,39 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.FbosConfig do
def maybe_flash_firmware(_state, "none", _old_hardware) do
Config.update_config_value(:bool, "settings", "firmware_needs_flash", false)
Config.update_config_value(:bool, "settings", "firmware_needs_open", true)
:ok
nil
end
def maybe_flash_firmware(_state, nil, _old_hardware) do
FarmbotCore.Logger.warn 1, "Firmware hardware unset. Not flashing"
:ok
nil
end
def maybe_flash_firmware(_state, new_hardware, old_hardware) do
def maybe_flash_firmware(state, new_hardware, old_hardware) do
force? = Config.get_config_value(:bool, "settings", "firmware_needs_flash")
ref = make_ref()
cond do
force? ->
DepTracker.register_asset(state.fbos_config, :firmware_flash)
FarmbotCore.Logger.warn 1, "Firmware hardware forced flash"
Config.update_config_value(:bool, "settings", "firmware_needs_flash", false)
new_hardware
|> fbos_config_to_flash_firmware_rpc()
|> FarmbotCeleryScript.execute(make_ref())
|> FarmbotCeleryScript.execute(ref)
ref
new_hardware != old_hardware ->
DepTracker.register_asset(state.fbos_config, :firmware_flash)
FarmbotCore.Logger.warn 1, "Firmware hardware change from #{package_to_string(old_hardware)} to #{package_to_string(new_hardware)}"
new_hardware
|> fbos_config_to_flash_firmware_rpc()
|> FarmbotCeleryScript.execute(make_ref())
|> FarmbotCeleryScript.execute(ref)
ref
true ->
DepTracker.register_asset(state.fbos_config, :idle)
# Config.update_config_value(:bool, "settings", "firmware_needs_open", true)
:ok
nil
end
end
@ -186,7 +237,8 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.FbosConfig do
:sequence_body_log,
:sequence_complete_log,
:sequence_init_log,
:update_channel
:update_channel,
:boot_sequence_id
]
new_interesting_fbos_config = Map.take(new_fbos_config, interesting_params) |> MapSet.new()
old_interesting_fbos_config = Map.take(old_fbos_config, interesting_params) |> MapSet.new()
@ -234,6 +286,17 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.FbosConfig do
{:sequence_init_log, bool} ->
FarmbotCore.Logger.success 1, "Set sequence init log messages to #{bool}"
{:boot_sequence_id, nil} ->
FarmbotCore.Logger.success 1, "Set bootup sequence to none"
{:boot_sequence_id, id} ->
case Asset.get_sequence(id) do
%{name: name} ->
FarmbotCore.Logger.success 1, "Set bootup sequence to #{name}"
_ ->
FarmbotCore.Logger.success 1, "Set bootup sequence"
end
{param, value} ->
FarmbotCore.Logger.success 1, "Set #{param} to #{value}"
end)
@ -263,7 +326,7 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.FbosConfig do
def fbos_config_to_flash_firmware_rpc(firmware_hardware) do
AST.Factory.new()
|> AST.Factory.rpc_request("FbosConfig")
|> AST.Factory.rpc_request("fbos_config.flash_firmware")
|> AST.Factory.flash_firmware(firmware_hardware)
end
end

View File

@ -1,12 +1,20 @@
defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.Peripheral do
use GenServer
require Logger
require FarmbotCore.Logger
alias FarmbotCore.{Asset.Peripheral, BotState}
alias FarmbotCore.{Asset.FbosConfig, Asset.Peripheral, BotState, DepTracker}
alias FarmbotCeleryScript.AST
@retry_ms 1_000
@unacceptable_fbos_config_statuses [
nil,
:init,
:firmware_flash,
:bootup_sequence,
]
@impl true
def preload(%Peripheral{}), do: []
@ -20,42 +28,83 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.Peripheral do
@impl true
def init(peripheral) do
%{informational_settings: %{idle: idle, firmware_version: fw_version}} = BotState.subscribe()
state = %{peripheral: peripheral, errors: 0, fw_idle: idle || false, fw_version: fw_version}
send self(), :timeout
:ok = DepTracker.register_asset(peripheral, :init)
%{
informational_settings: %{
idle: idle,
firmware_version: fw_version,
firmware_configured: fw_configured
}
} = BotState.subscribe()
state = %{
peripheral: peripheral,
errors: 0,
fw_idle: idle || false,
fw_version: fw_version,
fw_configured: fw_configured || false,
fbos_config_status: nil
}
:ok = DepTracker.subscribe_asset(FbosConfig)
send self(), :try_read_peripheral
{:ok, state}
end
@impl true
def handle_info(:timeout, %{fw_version: nil} = state) do
# Logger.debug("Not reading peripheral. Firmware not started.")
Process.send_after(self(), :timeout, @retry_ms)
def handle_info({DepTracker, {FbosConfig, _}, _old, status}, state) do
{:noreply, %{state | fbos_config_status: status}}
end
def handle_info(:try_read_peripheral, %{fbos_config_status: fbos_config_status} = state)
when fbos_config_status in @unacceptable_fbos_config_statuses do
# Logger.debug("Not reading peripheral. fbos_config not in acceptable state: #{fbos_config_status}")
Process.send_after(self(), :try_read_peripheral, @retry_ms)
{:noreply, state}
end
def handle_info(:timeout, %{fw_version: "8.0.0.S.stub"} = state) do
def handle_info(:try_read_peripheral, %{fbos_config_status: :bootup_sequence} = state) do
# Logger.debug("Not reading peripheral. Bootup sequence not complete")
Process.send_after(self(), :try_read_peripheral, @retry_ms)
{:noreply, state}
end
def handle_info(:timeout, %{fw_idle: false} = state) do
def handle_info(:try_read_peripheral, %{fw_version: nil} = state) do
# Logger.debug("Not reading peripheral. Firmware not booted.")
Process.send_after(self(), :try_read_peripheral, @retry_ms)
{:noreply, state}
end
def handle_info(:try_read_peripheral, %{fw_version: "none"} = state) do
# Logger.debug("Not reading peripheral. Firmware not booted.")
Process.send_after(self(), :try_read_peripheral, @retry_ms)
{:noreply, state}
end
def handle_info(:try_read_peripheral, %{fw_configured: false} = state) do
# Logger.debug("Not reading peripheral. Firmware not configured.")
Process.send_after(self(), :try_read_peripheral, @retry_ms)
{:noreply, state}
end
def handle_info(:try_read_peripheral, %{fw_idle: false} = state) do
# Logger.debug("Not reading peripheral. Firmware not idle.")
Process.send_after(self(), :timeout, @retry_ms)
Process.send_after(self(), :try_read_peripheral, @retry_ms)
{:noreply, state}
end
def handle_info(:timeout, %{peripheral: peripheral, errors: errors} = state) do
def handle_info(:try_read_peripheral, %{peripheral: peripheral, errors: errors} = state) do
Logger.debug("Read peripheral: #{peripheral.label}")
rpc = peripheral_to_rpc(peripheral)
case FarmbotCeleryScript.execute(rpc, make_ref()) do
:ok ->
:ok ->
:ok = DepTracker.register_asset(peripheral, :complete)
Logger.debug("Read peripheral: #{peripheral.label} ok")
{:noreply, state}
{:error, reason} when errors < 5 ->
Logger.error("Read peripheral: #{peripheral.label} error: #{reason} errors=#{state.errors}")
Process.send_after(self(), :timeout, @retry_ms)
{:error, reason} when errors < 5 ->
Logger.error("Read peripheral: #{peripheral.label} error: #{reason} errors=#{state.errors} status=#{state.fbos_config_status}")
Process.send_after(self(), :try_read_peripheral, @retry_ms)
{:noreply, %{state | errors: state.errors + 1}}
{:error, reason} when errors == 5 ->
Logger.error("Read peripheral: #{peripheral.label} error: #{reason} errors=5 not trying again.")
{:noreply, state}
@ -70,6 +119,15 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.Peripheral do
{:noreply, %{state | fw_version: fw_version}}
end
def handle_info({BotState, %{changes: %{informational_settings: %{changes: %{firmware_configured: fw_configured}}}}}, state) do
# this should really be fixed upstream not to dispatch if version is none.
if state.fw_version == "none" do
{:noreply, state}
else
{:noreply, %{state | fw_configured: fw_configured}}
end
end
def handle_info({BotState, _}, state) do
{:noreply, state}
end
@ -80,7 +138,7 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.Peripheral do
def peripheral_to_rpc(peripheral) do
AST.Factory.new()
|> AST.Factory.rpc_request(peripheral.local_id)
|> AST.Factory.rpc_request("peripheral." <> peripheral.local_id)
|> AST.Factory.set_pin_io_mode(peripheral.pin, "output")
|> AST.Factory.read_pin(peripheral.pin, peripheral.mode)
end

View File

@ -3,12 +3,12 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.PinBinding do
Worker for monitoring hardware GPIO. (not related to the mcu firmware.)
Upon a button trigger, a `sequence`, or `special_action` will be executed by
the CeleryScript Runtime.
the CeleryScript Runtime.
This module also defines a behaviour that allows for abstracting and testing
This module also defines a behaviour that allows for abstracting and testing
independent of GPIO hardware code.
"""
use GenServer
require Logger
require FarmbotCore.Logger
@ -75,7 +75,6 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.PinBinding do
case Asset.get_sequence(pin_binding.sequence_id) do
%Sequence{name: name} = seq ->
FarmbotCore.Logger.info(1, "#{pin_binding} triggered, executing #{name}")
AST.decode(seq)
|> execute(state)
@ -85,24 +84,24 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.PinBinding do
end
end
def handle_cast(
:trigger,
%{pin_binding: %{special_action: "emergency_lock"} = pin_binding} = state
) do
FarmbotCore.Logger.info(1, "#{pin_binding} triggered, executing Emergency Lock")
def handle_cast(:trigger, %{pin_binding: %{special_action: "dump_info"} = pin_binding} = state) do
FarmbotCore.Logger.info(1, "#{pin_binding} triggered, executing Dump Info")
AST.Factory.new()
|> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}")
|> AST.Factory.dump_info()
|> execute(state)
end
def handle_cast(:trigger, %{pin_binding: %{special_action: "emergency_lock"} = pin_binding} = state) do
FarmbotCore.Logger.info(1, "#{pin_binding} triggered, executing Emergency Lock")
AST.Factory.new()
|> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}")
|> AST.Factory.emergency_lock()
|> execute(state)
end
end
def handle_cast(
:trigger,
%{pin_binding: %{special_action: "emergency_unlock"} = pin_binding} = state
) do
def handle_cast(:trigger, %{pin_binding: %{special_action: "emergency_unlock"} = pin_binding} = state) do
FarmbotCore.Logger.info(1, "#{pin_binding} triggered, executing Emergency Unlock")
AST.Factory.new()
|> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}")
|> AST.Factory.emergency_unlock()
@ -111,19 +110,14 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.PinBinding do
def handle_cast(:trigger, %{pin_binding: %{special_action: "power_off"} = pin_binding} = state) do
FarmbotCore.Logger.info(1, "#{pin_binding} triggered, executing Power Off")
AST.Factory.new()
|> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}")
|> AST.Factory.power_off()
|> execute(state)
end
def handle_cast(
:trigger,
%{pin_binding: %{special_action: "read_status"} = pin_binding} = state
) do
def handle_cast(:trigger, %{pin_binding: %{special_action: "read_status"} = pin_binding} = state) do
FarmbotCore.Logger.info(1, "#{pin_binding} triggered, executing Read Status")
AST.Factory.new()
|> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}")
|> AST.Factory.read_status()
@ -132,7 +126,6 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.PinBinding do
def handle_cast(:trigger, %{pin_binding: %{special_action: "reboot"} = pin_binding} = state) do
FarmbotCore.Logger.info(1, "#{pin_binding} triggered, executing Reboot")
AST.Factory.new()
|> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}")
|> AST.Factory.reboot()
@ -141,7 +134,6 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.PinBinding do
def handle_cast(:trigger, %{pin_binding: %{special_action: "sync"} = pin_binding} = state) do
FarmbotCore.Logger.info(1, "#{pin_binding} triggered, executing Sync")
AST.Factory.new()
|> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}")
|> AST.Factory.sync()
@ -150,7 +142,6 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.PinBinding do
def handle_cast(:trigger, %{pin_binding: %{special_action: "take_photo"} = pin_binding} = state) do
FarmbotCore.Logger.info(1, "#{pin_binding} triggered, executing Take Photo")
AST.Factory.new()
|> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}")
|> AST.Factory.take_photo()
@ -191,13 +182,10 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.PinBinding do
defp execute(%AST{} = ast, state) do
case FarmbotCeleryScript.execute(ast, make_ref()) do
:ok ->
:ok
:ok -> :ok
{:error, reason} ->
FarmbotCore.Logger.error(1, "error executing #{state.pin_binding}: #{reason}")
FarmbotCore.Logger.error 1, "error executing #{state.pin_binding}: #{reason}"
end
{:noreply, state}
end

View File

@ -25,6 +25,8 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.RegimenInstance do
@impl GenServer
def init([regimen_instance, _args]) do
Logger.warn "RegimenInstance #{inspect(regimen_instance)} initializing"
with %Regimen{} <- regimen_instance.regimen,
%FarmEvent{} <- regimen_instance.farm_event do
send self(), :schedule
@ -38,25 +40,25 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.RegimenInstance do
def handle_info(:schedule, state) do
regimen_instance = state.regimen_instance
# load the sequence and calculate the scheduled_at time
Enum.map(regimen_instance.regimen.regimen_items, fn(%{time_offset: offset, sequence_id: sequence_id}) ->
Enum.map(regimen_instance.regimen.regimen_items, fn(%{time_offset: offset, sequence_id: sequence_id}) ->
scheduled_at = DateTime.add(regimen_instance.epoch, offset, :millisecond)
sequence = Asset.get_sequence(sequence_id) || raise("sequence #{sequence_id} is not synced")
%{scheduled_at: scheduled_at, sequence: sequence}
end)
# get rid of any item that has already been scheduled/executed
|> Enum.reject(fn(%{scheduled_at: scheduled_at}) ->
|> Enum.reject(fn(%{scheduled_at: scheduled_at}) ->
Asset.get_regimen_instance_execution(regimen_instance, scheduled_at)
end)
|> Enum.each(fn(%{scheduled_at: at, sequence: sequence}) ->
|> Enum.each(fn(%{scheduled_at: at, sequence: sequence}) ->
schedule_sequence(regimen_instance, sequence, at)
end)
{:noreply, state}
{:noreply, state}
end
def handle_info({FarmbotCeleryScript, {:scheduled_execution, scheduled_at, executed_at, result}}, state) do
status = case result do
:ok -> "ok"
{:error, reason} ->
{:error, reason} ->
FarmbotCore.Logger.error(2, "Regimen scheduled at #{scheduled_at} failed to execute: #{reason}")
reason
end
@ -79,11 +81,11 @@ defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.RegimenInstance do
regimen_params = AST.decode(regimen_instance.regimen.body)
# there may be many sequence scopes from here downward
celery_ast = AST.decode(sequence)
celery_args =
celery_args =
celery_ast.args
|> Map.put(:sequence_name, sequence.name)
|> Map.put(:locals, %{celery_ast.args.locals | body: celery_ast.args.locals.body ++ regimen_params ++ farm_event_params})
celery_ast = %{celery_ast | args: celery_args}
FarmbotCeleryScript.schedule(celery_ast, at, sequence)
end

View File

@ -1,8 +1,6 @@
defmodule FarmbotCore.BotState do
@moduledoc "Central State accumulator."
alias FarmbotCore.BotStateNG
alias FarmbotCore.BotState.JobProgress.Percent
require FarmbotCore.Logger
use GenServer
@ -30,7 +28,7 @@ defmodule FarmbotCore.BotState do
def set_position(bot_state_server \\ __MODULE__, x, y, z) do
GenServer.call(bot_state_server, {:set_position, x, y, z})
end
@doc "Sets the location_data.load"
def set_load(bot_state_server \\ __MODULE__, x, y, z) do
GenServer.call(bot_state_server, {:set_load, x, y, z})
@ -74,6 +72,10 @@ defmodule FarmbotCore.BotState do
def set_firmware_version(bot_state_server \\ __MODULE__, version) do
GenServer.call(bot_state_server, {:set_firmware_version, version})
end
def set_firmware_configured(bot_state_server \\ __MODULE__, configured) do
GenServer.call(bot_state_server, {:set_firmware_configured, configured})
end
@doc "Sets configuration.arduino_hardware"
def set_firmware_hardware(bot_state_server \\ __MODULE__, hardware) do
@ -163,10 +165,6 @@ defmodule FarmbotCore.BotState do
GenServer.call(bot_state_server, :enter_maintenance_mode)
end
def job_in_progress?(job_name, bot_state_server \\ __MODULE__) do
GenServer.call(bot_state_server, {:job_in_progress?, job_name})
end
@doc false
def start_link(args, opts \\ [name: __MODULE__]) do
GenServer.start_link(__MODULE__, args, opts)
@ -181,13 +179,6 @@ defmodule FarmbotCore.BotState do
FarmbotCore.Logger.error 1, "BotState crashed! #{inspect(reason)}"
end
def handle_call({:job_in_progress?, job_name}, _from, state) do
progress = (state.tree.jobs[job_name] || %Percent{}).percent
in_progress? = (progress > 0.0 && progress < 100.0)
{:reply, in_progress?, state}
end
@doc false
def handle_call(:subscribe, {pid, _} = _from, state) do
# TODO Just replace this with Elixir.Registry?
@ -319,6 +310,17 @@ defmodule FarmbotCore.BotState do
{:reply, reply, state}
end
def handle_call({:set_firmware_configured, configured}, _from, state) do
change = %{informational_settings: %{firmware_configured: configured}}
{reply, state} =
BotStateNG.changeset(state.tree, change)
|> dispatch_and_apply(state)
{:reply, reply, state}
end
def handle_call({:set_firmware_hardware, hardware}, _from, state) do
change = %{configuration: %{firmware_hardware: hardware}}

View File

@ -6,16 +6,11 @@ defmodule FarmbotCore.BotState.Supervisor do
end
def init([]) do
Supervisor.init(children(), [strategy: :one_for_all])
end
def children do
default = [
children = [
FarmbotCore.BotState,
FarmbotCore.BotState.FileSystem,
FarmbotCore.BotState.SchedulerUsageReporter
]
config = Application.get_env(:farmbot_ext, __MODULE__) || []
Keyword.get(config, :children, default)
Supervisor.init(children, [strategy: :one_for_all])
end
end

View File

@ -16,6 +16,7 @@ defmodule FarmbotCore.BotStateNG.InformationalSettings do
field(:controller_uuid, :string)
field(:controller_commit, :string, default: Project.commit())
field(:firmware_version, :string)
field(:firmware_configured, :boolean, default: false)
field(:node_name, :string)
field(:private_ip, :string)
field(:soc_temp, :integer)
@ -52,6 +53,7 @@ defmodule FarmbotCore.BotStateNG.InformationalSettings do
commit: informational_settings.controller_commit,
firmware_commit: informational_settings.firmware_commit,
firmware_version: informational_settings.firmware_version,
firmware_configured: informational_settings.firmware_configured,
node_name: informational_settings.node_name,
private_ip: informational_settings.private_ip,
soc_temp: informational_settings.soc_temp,
@ -84,6 +86,7 @@ defmodule FarmbotCore.BotStateNG.InformationalSettings do
:controller_commit,
:firmware_commit,
:firmware_version,
:firmware_configured,
:node_name,
:private_ip,
:soc_temp,

View File

@ -13,7 +13,7 @@ defmodule FarmbotCore.BotStateNG.LocationData.Vec3 do
def new do
%__MODULE__{}
|> changeset(%{x: nil, y: nil, z: nil})
|> changeset(%{x: -1, y: -1, z: -1})
|> apply_changes()
end
@ -29,4 +29,4 @@ defmodule FarmbotCore.BotStateNG.LocationData.Vec3 do
vec3
|> cast(params, [:x, :y, :z])
end
end
end

View File

@ -210,7 +210,7 @@ defmodule FarmbotCore.BotStateNG.McuParams do
pin_guard_4_active_state: mcu_params.pin_guard_4_active_state,
pin_guard_5_pin_nr: mcu_params.pin_guard_5_pin_nr,
pin_guard_5_time_out: mcu_params.pin_guard_5_time_out,
pin_guard_5_active_state: mcu_params.pin_guard_5_active_state
pin_guard_5_active_state: :pin_guard_5_active_stat
}
end

View File

@ -7,12 +7,9 @@ defmodule FarmbotCore.Config.Supervisor do
end
def init([]) do
Supervisor.init(children(), strategy: :one_for_one)
end
def children do
default = [ {FarmbotCore.Config.Repo, []} ]
config = Application.get_env(:farmbot_ext, __MODULE__) || []
Keyword.get(config, :children, default)
children = [
{FarmbotCore.Config.Repo, []},
]
Supervisor.init(children, strategy: :one_for_one)
end
end

View File

@ -0,0 +1,81 @@
defmodule FarmbotCore.DepTracker do
@moduledoc """
Subscribe to internal dependency and service status events.
"""
alias FarmbotCore.{DepTracker, DepTracker.Table}
@doc "Start a dep tracker instance"
def start_link(options) do
name = Keyword.get(options, :name, DepTracker)
unless !is_nil(name) and is_atom(name) do
raise ArgumentError, "expected :name to be given and to be an atom, got: #{inspect(name)}"
end
DepTracker.Supervisor.start_link(name)
end
@doc false
def child_spec(opts) do
%{
id: Keyword.get(opts, :name, DepTracker),
start: {DepTracker, :start_link, [opts]},
type: :supervisor
}
end
@doc "register an asset in the tracker"
def register_asset(table \\ DepTracker, %kind{local_id: local_id}, status) do
Table.put(table, {{kind, local_id}, status})
end
@doc "register a service in the tracker"
def register_service(table \\ DepTracker, service_name, status) do
Table.put(table, {service_name, status})
end
@doc """
subscribe to asset changes from the tracker
messages are dispatched in the shape of
{table_name, {kind, local_id}, status}
"""
def subscribe_asset(table \\ DepTracker, kind) do
:ok = do_subscribe(table, kind)
initial = get_asset(table, kind)
for {{kind, local_id}, status} <- initial do
send self(), {table, {kind, local_id}, nil, status}
end
:ok
end
@doc "get all current assets by kind"
def get_asset(table \\ DepTracker, kind) do
Table.get(table, {kind, :"$1"})
end
@doc """
subscribe to service changes from the tracker
messages are dispatched in the shape of
{table_name, service_name, status}
"""
def subscribe_service(table \\ DepTracker, service_name) do
:ok = do_subscribe(table, service_name)
initial = get_service(table, service_name)
for {^service_name, status} <- initial do
send self(), {table, {service_name, nil, status}}
end
:ok
end
@doc "get all current services by name"
def get_service(table \\ DepTracker, service_name) do
Table.get(table, service_name)
end
defp do_subscribe(table, name) do
registry = DepTracker.Supervisor.registry_name(table)
{:ok, _} = Registry.register(registry, name, nil)
:ok
end
end

View File

@ -0,0 +1,52 @@
defmodule FarmbotCore.DepTracker.Logger do
alias FarmbotCore.DepTracker
require Logger
use GenServer
@doc false
def child_spec(args) do
%{
id: name(args),
start: {FarmbotCore.DepTracker.Logger, :start_link, [args]},
}
end
def start_link(args) do
GenServer.start_link(__MODULE__, args, [name: name(args)])
end
defp name({table, [service: service_name]}) do
Module.concat([__MODULE__, table, service_name])
end
defp name({table, [asset: kind]}) do
Module.concat([__MODULE__, table, kind])
end
def init({table, [service: service_name]}) do
:ok = DepTracker.subscribe_service(table, service_name)
{:ok, %{service: service_name, table: table}}
end
def init({table, [asset: kind]}) do
:ok = DepTracker.subscribe_asset(table, kind)
{:ok, %{asset: kind, table: table}}
end
def handle_info({table, {kind, local_id}, old_status, new_status}, %{asset: kind, table: table} = state) do
Logger.info """
#{inspect(table)} asset status change:
#{kind} local_id = #{local_id}
#{kind} #{inspect(old_status)} => #{inspect(new_status)}
"""
{:noreply, state}
end
def handle_info({table, service, old_status, new_status}, %{service: service, table: table} = state) do
Logger.info """
#{inspect(table)} service status change:
#{service} #{inspect(old_status)} => #{inspect(new_status)}
"""
{:noreply, state}
end
end

View File

@ -0,0 +1,29 @@
defmodule FarmbotCore.DepTracker.Supervisor do
use Supervisor
alias FarmbotCore.DepTracker
@moduledoc false
def start_link(name) do
Supervisor.start_link(__MODULE__, name)
end
@impl true
def init(name) do
registry_name = registry_name(name)
children = [
{DepTracker.Table, {name, registry_name}},
{Registry, [keys: :duplicate, name: registry_name]},
# {DepTracker.Logger, {name, service: :firmware}},
{DepTracker.Logger, {name, asset: FarmbotCore.Asset.FbosConfig}},
]
Supervisor.init(children, strategy: :one_for_one)
end
def registry_name(name) do
Module.concat(DepTracker.Registry, name)
end
end

View File

@ -0,0 +1,69 @@
defmodule FarmbotCore.DepTracker.Table do
use GenServer
@doc false
def start_link({table, _registry_name} = args) do
GenServer.start_link(__MODULE__, args, name: table)
end
@doc "put data"
def put(table, {identifier, status}) do
GenServer.call(table, {:put, identifier, status})
end
@doc "get data"
def get(table, {kind, _} = identifier) do
:ets.match(table, {identifier, :"$2"})
|> Enum.map(fn
[local_id, status] -> {{kind, local_id}, status}
other -> raise("unknown data in ets table: #{table} data: #{inspect(other)}")
end)
end
def get(table, service_name) do
:ets.match(table, {service_name, :"$2"})
|> Enum.map(fn
[status] -> {service_name, status}
other -> raise("unknown data in ets table: #{table} data: #{inspect(other)}")
end)
end
@impl GenServer
def init({table, registry_name}) do
^table = :ets.new(table, [:named_table, read_concurrency: true])
state = %{table: table, registry: registry_name}
{:ok, state}
end
@impl GenServer
def handle_call({:put, identifier, status}, _from, state) do
case :ets.lookup(state.table, identifier) do
[{^identifier, ^status}] ->
# No change, so no notifications
:ok
[{^identifier, old_status}] ->
:ets.insert(state.table, {identifier, status})
dispatch(state, identifier, old_status, status)
[] ->
:ets.insert(state.table, {identifier, status})
dispatch(state, identifier, nil, status)
end
{:reply, :ok, state}
end
defp dispatch(state, identifier, old, new) do
kind = case identifier do
{kind, _} -> kind
kind -> kind
end
Registry.dispatch(state.registry, kind, fn entries ->
message = {state.table, identifier, old, new}
for {pid, _} <- entries, do: send(pid, message)
end)
:ok
end
end

View File

@ -4,10 +4,10 @@ defmodule FarmbotCore.FarmwareRuntime do
"""
alias FarmbotCeleryScript.AST
alias FarmbotCore.Asset.FarmwareInstallation.Manifest
alias FarmbotCore.AssetWorker.FarmbotCore.Asset.FarmwareInstallation
alias FarmbotCore.BotState.FileSystem
alias FarmbotCore.FarmwareRuntime.PipeWorker
alias FarmbotCore.AssetWorker.FarmbotCore.Asset.FarmwareInstallation
alias FarmbotCore.Asset.FarmwareInstallation.Manifest
alias FarmbotCore.BotState.FileSystem
alias FarmbotCore.Project
import FarmwareInstallation, only: [install_dir: 1]
@ -67,8 +67,7 @@ defmodule FarmbotCore.FarmwareRuntime do
@doc "Stop a farmware"
def stop(pid) do
Logger.info("Terminating farmware process")
Logger.info "Terminating farmware process"
if Process.alive?(pid) do
GenServer.stop(pid, :normal)
end
@ -76,7 +75,7 @@ defmodule FarmbotCore.FarmwareRuntime do
def init([manifest, env, caller]) do
package = manifest.package
<<clause1::binary-size(8), _::binary>> = Ecto.UUID.generate()
<<clause1 :: binary-size(8), _::binary>> = Ecto.UUID.generate()
request_pipe =
Path.join([
@ -110,8 +109,7 @@ defmodule FarmbotCore.FarmwareRuntime do
)
# Start the plugin.
Logger.debug("spawning farmware: #{exec} #{manifest.args}")
Logger.debug "spawning farmware: #{exec} #{manifest.args}"
{cmd, _} = spawn_monitor(MuonTrap, :cmd, ["sh", ["-c", "#{exec} #{manifest.args}"], opts])
state = %State{
@ -127,7 +125,7 @@ defmodule FarmbotCore.FarmwareRuntime do
response_pipe_handle: resp
}
send(self(), :timeout)
send self(), :timeout
{:ok, state}
end
@ -144,23 +142,24 @@ defmodule FarmbotCore.FarmwareRuntime do
end
def handle_info(msg, %{context: :error} = state) do
Logger.warn("unhandled message in error state: #{inspect(msg)}")
Logger.warn "unhandled message in error state: #{inspect(msg)}"
{:noreply, state}
end
def handle_info({:step_complete, ref, {:error, reason}}, %{scheduler_ref: ref} = state) do
send(state.caller, {:error, reason})
send state.caller, {:error, reason}
{:noreply, %{state | ref: nil, context: :error}}
end
def handle_info({:step_complete, ref, :ok}, %{scheduler_ref: ref} = state) do
result = %AST{kind: :rpc_ok, args: %{label: state.rpc.args.label}, body: []}
label = UUID.uuid4()
result = %AST{kind: :rpc_ok, args: %{label: label}, body: []}
ipc = add_header(result)
_reply = PipeWorker.write(state.response_pipe_handle, ipc)
# Make sure to `timeout` after this one to go back to the
# get_header context. This will cause another rpc to be processed.
send(self(), :timeout)
send self(), :timeout
{:noreply, %{state | rpc: nil, context: :get_header}}
end
@ -175,14 +174,14 @@ defmodule FarmbotCore.FarmwareRuntime do
# didn't pick up the scheduled AST in a reasonable amount of time.
def handle_info(:timeout, %{context: :process_request} = state) do
Logger.error("Timeout waiting for #{inspect(state.rpc)} to be processed")
send(state.caller, {:error, :rpc_timeout})
send state.caller, {:error, :rpc_timeout}
{:noreply, %{state | context: :error}}
end
# farmware exit
def handle_info({:DOWN, _ref, :process, _pid, _reason}, %{cmd: _cmd_pid} = state) do
Logger.debug("Farmware exit")
send(state.caller, {:error, :farmware_exit})
send state.caller, {:error, :farmware_exit}
{:noreply, %{state | context: :error}}
end
@ -201,14 +200,14 @@ defmodule FarmbotCore.FarmwareRuntime do
# error result of an io:read/2 in :get_header context
def handle_info({PipeWorker, _ref, {:ok, data}}, %{context: :get_header} = state) do
Logger.error("Bad header: #{inspect(data, base: :hex, limit: :infinity)}")
send(state.caller, {:error, {:unhandled_packet, data}})
send state.caller, {:error, {:unhandled_packet, data}}
{:noreply, %{state | context: :error}}
end
# error result of an io:read/2 in :get_header context
def handle_info({PipeWorker, _ref, error}, %{context: :get_header} = state) do
Logger.error("Bad header: #{inspect(error)}")
send(state.caller, {:error, :bad_packet_header})
send state.caller, {:error, :bad_packet_header}
{:noreply, %{state | context: :error}}
end
@ -220,7 +219,7 @@ defmodule FarmbotCore.FarmwareRuntime do
# error result of an io:read/2 in :get_header context
def handle_info({PipeWorker, _ref, error}, %{context: :get_payload} = state) do
Logger.error("Bad payload: #{inspect(error)}")
send(state.caller, {:error, :bad_packet_payload})
send state.caller, {:error, :bad_packet_payload}
{:noreply, %{state | context: :error}}
end
@ -248,12 +247,10 @@ defmodule FarmbotCore.FarmwareRuntime do
Logger.debug("executing rpc from farmware: #{inspect(rpc)}")
# todo(connor) replace this with StepRunner?
FarmbotCeleryScript.execute(rpc, ref)
{:noreply, %{state | rpc: rpc, scheduler_ref: ref, context: :process_request},
@error_timeout_ms}
{:noreply, %{state | rpc: rpc, scheduler_ref: ref, context: :process_request}, @error_timeout_ms}
else
{:error, reason} ->
send(state.caller, {:error, reason})
send state.caller, {:error, reason}
{:noreply, %{state | context: :error}}
end
end
@ -303,7 +300,6 @@ defmodule FarmbotCore.FarmwareRuntime do
header =
<<@packet_header_token::size(16)>> <>
:binary.copy(<<0x00>>, 4) <> <<byte_size(payload)::big-size(32)>>
header <> payload
end
end

View File

@ -8,8 +8,15 @@ defmodule FarmbotCore.FirmwareOpenTask do
use GenServer
require FarmbotCore.Logger
alias FarmbotFirmware.{UARTTransport, StubTransport}
alias FarmbotCore.{Asset, Config}
@attempt_threshold Application.get_env(:farmbot_core, __MODULE__)[:attempt_threshold] || 5
alias FarmbotCore.{Asset, Config, DepTracker}
@attempt_threshold Application.get_env(:farmbot_core, __MODULE__)[:attempt_threshold]
@attempt_threshold || Mix.raise """
Firmware open attempt threshold not configured:
config :farmbot_core, FarmbotCore.FirmwareOpenTask, [
attempt_threshold: 10
]
"""
@doc false
def start_link(args, opts \\ [name: __MODULE__]) do
@ -18,10 +25,7 @@ defmodule FarmbotCore.FirmwareOpenTask do
@doc false
def swap_transport(tty) do
Application.put_env(:farmbot_firmware, FarmbotFirmware,
transport: UARTTransport,
device: tty,
reset: FarmbotCore.FirmwareResetter)
Application.put_env(:farmbot_firmware, FarmbotFirmware, transport: UARTTransport, device: tty)
# Swap transport on FW module.
# Close tranpsort if it is open currently.
_ = FarmbotFirmware.close_transport()
@ -29,9 +33,7 @@ defmodule FarmbotCore.FirmwareOpenTask do
end
def unswap_transport() do
Application.put_env(:farmbot_firmware, FarmbotFirmware,
transport: StubTransport,
reset: FarmbotCore.FirmwareResetter)
Application.put_env(:farmbot_firmware, FarmbotFirmware, transport: StubTransport)
# Swap transport on FW module.
# Close tranpsort if it is open currently.
_ = FarmbotFirmware.close_transport()
@ -75,21 +77,22 @@ defmodule FarmbotCore.FirmwareOpenTask do
{:noreply, increment_attempts(%{state | timer: timer})}
firmware_hardware == "none" && needs_open? ->
FarmbotCore.Logger.debug 3, "Closing firmware..."
FarmbotCore.Logger.debug 3, "Firmware needs to be closed"
unswap_transport()
Config.update_config_value(:bool, "settings", "firmware_needs_open", false)
timer = Process.send_after(self(), :open, 5000)
{:noreply, %{state | timer: timer, attempts: 0}}
needs_open? ->
FarmbotCore.Logger.debug 3, "Opening firmware..."
FarmbotCore.Logger.debug 3, "Firmware needs to be opened"
case swap_transport(firmware_path) do
:ok ->
Config.update_config_value(:bool, "settings", "firmware_needs_open", false)
timer = Process.send_after(self(), :open, 5000)
DepTracker.register_service(:firmware, :init)
{:noreply, %{state | timer: timer, attempts: 0}}
other ->
FarmbotCore.Logger.debug 3, "Not ready to open yet, will retry in 5s (#{inspect(other)})"
_ ->
FarmbotCore.Logger.debug 3, "Firmware failed to open"
timer = Process.send_after(self(), :open, 5000)
{:noreply, %{state | timer: timer, attempts: 0}}
end

View File

@ -1,46 +0,0 @@
defmodule FarmbotCore.FirmwareResetter do
if Code.ensure_compiled?(Circuits.GPIO) do
@gpio Circuits.GPIO
else
@gpio nil
end
alias FarmbotCore.Asset
require FarmbotCore.Logger
def reset(package \\ nil) do
pkg = package || Asset.fbos_config(:firmware_hardware)
FarmbotCore.Logger.debug(3, "Attempting to retrieve #{pkg} reset function.")
{:ok, fun} = find_reset_fun(pkg)
fun.()
end
def find_reset_fun("express_k10") do
FarmbotCore.Logger.debug(3, "Using special express reset function")
{:ok, fn -> express_reset_fun() end}
end
def find_reset_fun(_) do
FarmbotCore.Logger.debug(3, "Using default reset function")
{:ok, fn -> :ok end}
end
def express_reset_fun() do
try do
gpio_module = @gpio
FarmbotCore.Logger.debug(3, "Begin MCU reset")
{:ok, gpio} = gpio_module.open(19, :output)
:ok = gpio_module.write(gpio, 0)
:ok = gpio_module.write(gpio, 1)
Process.sleep(1000)
:ok = gpio_module.write(gpio, 0)
FarmbotCore.Logger.debug(3, "Finish MCU Reset")
:ok
rescue
ex ->
message = Exception.message(ex)
msg = "Express reset failed #{message}"
FarmbotCore.Logger.error(3, msg)
{:error, msg}
end
end
end

View File

@ -3,7 +3,7 @@ defmodule FarmbotCore.FirmwareSideEffects do
@behaviour FarmbotFirmware.SideEffects
require Logger
require FarmbotCore.Logger
alias FarmbotCore.{Asset, BotState, FirmwareEstopTimer, Leds}
alias FarmbotCore.{Asset, BotState, DepTracker, FirmwareEstopTimer, Leds}
@impl FarmbotFirmware.SideEffects
def handle_position(x: x, y: y, z: z) do
@ -16,14 +16,7 @@ defmodule FarmbotCore.FirmwareSideEffects do
end
@impl FarmbotFirmware.SideEffects
def handle_position_change([{axis, 0.0}]) do
FarmbotCore.Logger.warn(1, "#{axis}-axis stopped at home")
:noop
end
@impl FarmbotFirmware.SideEffects
def handle_position_change([{axis, _}]) do
FarmbotCore.Logger.warn(1, "#{axis}-axis stopped at maximum")
def handle_position_change([{_axis, _value}]) do
:noop
end
@ -34,7 +27,7 @@ defmodule FarmbotCore.FirmwareSideEffects do
@impl FarmbotFirmware.SideEffects
def handle_axis_timeout(axis) do
FarmbotCore.Logger.error(1, "#{axis}-axis timed out waiting for movement to complete")
FarmbotCore.Logger.error 1, "Axis #{axis} timed out waiting for movement to complete"
:noop
end
@ -55,7 +48,7 @@ defmodule FarmbotCore.FirmwareSideEffects do
# this is a bug in the firmware code i think
def handle_encoders_scaled([]), do: :noop
@impl FarmbotFirmware.SideEffects
def handle_encoders_raw(x: x, y: y, z: z) do
:ok = BotState.set_encoders_raw(x, y, z)
@ -71,7 +64,6 @@ defmodule FarmbotCore.FirmwareSideEffects do
%{param => value}
|> Asset.update_firmware_config!()
|> Asset.Private.mark_dirty!(%{})
:ok
end
@ -83,56 +75,31 @@ defmodule FarmbotCore.FirmwareSideEffects do
@impl FarmbotFirmware.SideEffects
def handle_software_version([version]) do
:ok = BotState.set_firmware_version(version)
case String.split(version, ".") do
# Ramps
[_, _, _, "R"] ->
_ = Leds.red(:solid)
:ok = BotState.set_firmware_hardware("arduino")
[_, _, _, "R", _] ->
_ = Leds.red(:solid)
:ok = BotState.set_firmware_hardware("arduino")
# Farmduino
[_, _, _, "F"] ->
_ = Leds.red(:solid)
:ok = BotState.set_firmware_hardware("farmduino")
[_, _, _, "F", _] ->
_ = Leds.red(:solid)
:ok = BotState.set_firmware_hardware("farmduino")
# Farmduino V14
[_, _, _, "G"] ->
_ = Leds.red(:solid)
:ok = BotState.set_firmware_hardware("farmduino_k14")
[_, _, _, "G", _] ->
_ = Leds.red(:solid)
:ok = BotState.set_firmware_hardware("farmduino_k14")
# Farmduino V15
[_, _, _, "H"] ->
_ = Leds.red(:solid)
:ok = BotState.set_firmware_hardware("farmduino_k15")
[_, _, _, "H", _] ->
_ = Leds.red(:solid)
:ok = BotState.set_firmware_hardware("farmduino_k15")
# Express V10
[_, _, _, "E"] ->
_ = Leds.red(:solid)
:ok = BotState.set_firmware_hardware("express_k10")
[_, _, _, "E", _] ->
_ = Leds.red(:solid)
:ok = BotState.set_firmware_hardware("express_k10")
[_, _, _, "S"] ->
_ = Leds.red(:slow_blink)
:ok = BotState.set_firmware_version("none")
:ok = BotState.set_firmware_hardware("none")
[_, _, _, "S", _] ->
_ = Leds.red(:slow_blink)
:ok = BotState.set_firmware_version("none")
:ok = BotState.set_firmware_hardware("none")
end
end
@ -144,6 +111,7 @@ defmodule FarmbotCore.FirmwareSideEffects do
@impl FarmbotFirmware.SideEffects
def handle_busy(busy) do
:ok = BotState.set_firmware_busy(busy)
DepTracker.register_service(:firmware, :busy)
end
@impl FarmbotFirmware.SideEffects
@ -151,6 +119,7 @@ defmodule FarmbotCore.FirmwareSideEffects do
_ = FirmwareEstopTimer.cancel_timer()
:ok = BotState.set_firmware_unlocked()
:ok = BotState.set_firmware_idle(idle)
DepTracker.register_service(:firmware, :idle)
end
@impl FarmbotFirmware.SideEffects
@ -158,6 +127,7 @@ defmodule FarmbotCore.FirmwareSideEffects do
_ = FirmwareEstopTimer.start_timer()
_ = Leds.yellow(:slow_blink)
:ok = BotState.set_firmware_locked()
DepTracker.register_service(:firmware, :locked)
end
@impl FarmbotFirmware.SideEffects
@ -192,15 +162,14 @@ defmodule FarmbotCore.FirmwareSideEffects do
@impl FarmbotFirmware.SideEffects
def handle_debug_message([message]) do
fbos_config = Asset.fbos_config()
should_log? = fbos_config.firmware_debug_log || fbos_config.arduino_debug_messages
should_log? && do_send_debug_message(message)
should_log? = fbos_config.firmware_debug_log || fbos_config.arduino_debug_messages
should_log? && FarmbotCore.Logger.debug(3, "Firmware debug message: " <> message)
end
# TODO(Rick): 0 means OK, but firmware debug logs say "error 0". Why?
def do_send_debug_message("error 0"), do: do_send_debug_message("OK")
def do_send_debug_message(message) do
FarmbotCore.Logger.debug(3, "Firmware debug message: " <> message)
@impl FarmbotFirmware.SideEffects
def handle_configuration_status(status) do
:ok = BotState.set_firmware_configured(status)
DepTracker.register_service(:firmware, :configured)
end
@impl FarmbotFirmware.SideEffects

View File

@ -3,13 +3,16 @@ defmodule FarmbotCore.FirmwareTTYDetector do
require Logger
alias Circuits.UART
@error_retry_ms 5_000
@expected_names Application.get_env(:farmbot_core, __MODULE__)[:expected_names]
@expected_names ||
Mix.raise("""
Please configure `expected_names` for TTYDetector.
if System.get_env("FARMBOT_TTY") do
@expected_names ["ttyUSB0", "ttyAMA0", "ttyACM0", System.get_env("FARMBOT_TTY")]
else
@expected_names ["ttyUSB0", "ttyAMA0", "ttyACM0"]
end
config :farmbot_core, FarmbotCore.FirmwareTTYDetector,
expected_names: ["ttyS0", "ttyNotReal"]
""")
@error_retry_ms 5_000
@doc "Gets the detected TTY"
def tty(server \\ __MODULE__) do
@ -50,6 +53,7 @@ defmodule FarmbotCore.FirmwareTTYDetector do
if farmbot_tty?(name) do
{:noreply, name}
else
# Logger.warn("#{name} is not an expected Farmbot Firmware TTY")
{:noreply, state, {:continue, rest}}
end
end

View File

@ -1,5 +1,7 @@
defmodule FarmbotCore.Leds do
@moduledoc "API for controling Farmbot LEDS."
@led_handler Application.get_env(:farmbot_core, __MODULE__)[:gpio_handler]
@led_handler || Mix.raise("You forgot a led handler!")
@valid_status [:off, :solid, :slow_blink, :fast_blink, :really_fast_blink]
@ -13,7 +15,29 @@ defmodule FarmbotCore.Leds do
def white4(status) when status in @valid_status, do: led_handler().white4(status)
def white5(status) when status in @valid_status, do: led_handler().white5(status)
def led_handler,
def factory_test(status) do
red(:off)
blue(:off)
green(:off)
yellow(:off)
white1(:off)
white2(:off)
white3(:off)
white4(:off)
white5(:off)
red(status)
blue(status)
green(status)
yellow(status)
white1(status)
white2(status)
white3(status)
white4(status)
white5(status)
end
defp led_handler,
do: Application.get_env(:farmbot_core, __MODULE__)[:gpio_handler]
def child_spec(opts) do

View File

@ -13,8 +13,7 @@ defmodule FarmbotCore.Leds.StubHandler do
def white5(status), do: do_debug(:white, status)
defp do_debug(color, status) do
unless System.get_env("LOG_SILENCE") do
msg = [IO.ANSI.reset(), "LED STATUS: ",
msg = [IO.ANSI.reset(), "LED STATUS: ",
apply(IO.ANSI, color, []),
status_in(status),
to_string(color),
@ -23,8 +22,7 @@ defmodule FarmbotCore.Leds.StubHandler do
status_out(status),
IO.ANSI.reset()
]
IO.puts(msg)
end
IO.puts(msg)
end
defp status_in(:slow_blink), do: IO.ANSI.blink_slow()

View File

@ -1,31 +0,0 @@
# This module could have existed within FarmbotCore.Logger.
# Pulling this function into a different module facilitates
# mocking of tests.
defmodule FarmbotCore.LogExecutor do
alias FarmbotCore.Log
def execute(%Log{} = log) do
logger_meta = [
application: :farmbot,
function: log.function,
file: log.file,
line: log.line,
module: log.module,
channels: log.meta[:channels] || log.meta["channels"],
verbosity: log.verbosity,
assertion_passed: log.meta[:assertion_passed]
]
level = log.level
logger_level =
if level in [:info, :debug, :warn, :error],
do: level,
else: :info
unless System.get_env("LOG_SILENCE") do
Elixir.Logger.bare_log(logger_level, log, logger_meta)
end
log
end
end

View File

@ -5,7 +5,6 @@ defmodule FarmbotCore.Logger do
alias FarmbotCore.{Log, Logger.Repo}
import Ecto.Query
@log_types [:info, :debug, :busy, :warn, :success, :error, :fun, :assertion]
@doc "Send a debug message to log endpoints"
defmacro debug(verbosity, message, meta \\ []) do
@ -58,27 +57,23 @@ defmodule FarmbotCore.Logger do
def insert_log!(params) do
changeset = Log.changeset(%Log{}, params)
try do
hash = Ecto.Changeset.get_field(changeset, :hash)
case Repo.get_by(Log, hash: hash) do
nil ->
nil ->
Repo.insert!(changeset)
old ->
params =
old ->
params =
params
|> Map.put(:inserted_at, DateTime.utc_now())
|> Map.put(:inserted_at, DateTime.utc_now)
|> Map.put(:duplicates, old.duplicates + 1)
old
|> Log.changeset(params)
|> Repo.update!()
old
|> Log.changeset(params)
|> Repo.update!()
end
catch
kind, err ->
IO.warn("Error inserting log: #{kind} #{inspect(err)}", __STACKTRACE__)
IO.warn("Error inserting log: #{kind} #{inspect(err)}", __STACKTRACE__)
Ecto.Changeset.apply_changes(changeset)
end
end
@ -99,16 +94,13 @@ defmodule FarmbotCore.Logger do
@doc false
def dispatch_log(%Macro.Env{} = env, level, verbosity, message, meta)
when level in @log_types and
is_number(verbosity) and
is_binary(message) and
is_list(meta) do
when level in [:info, :debug, :busy, :warn, :success, :error, :fun, :assertion] and is_number(verbosity) and
is_binary(message) and is_list(meta) do
fun =
case env.function do
{fun, ar} -> "#{fun}/#{ar}"
nil -> "no_function"
end
%{
level: level,
verbosity: verbosity,
@ -124,8 +116,29 @@ defmodule FarmbotCore.Logger do
@doc false
def dispatch_log(params) do
log = insert_log!(params)
FarmbotCore.LogExecutor.execute(log)
params
|> insert_log!()
|> elixir_log()
end
defp elixir_log(%Log{} = log) do
logger_meta = [
application: :farmbot,
function: log.function,
file: log.file,
line: log.line,
module: log.module,
channels: log.meta[:channels] || log.meta["channels"],
verbosity: log.verbosity,
assertion_passed: log.meta[:assertion_passed]
# TODO Connor - fix time
# time: time
]
level = log.level
logger_level = if level in [:info, :debug, :warn, :error], do: level, else: :info
Elixir.Logger.bare_log(logger_level, log, logger_meta)
log
end
@doc "Helper function for deciding if a message should be logged or not."

View File

@ -7,13 +7,11 @@ defmodule FarmbotCore.Logger.Supervisor do
end
def init([]) do
opts = [strategy: :one_for_all]
supervise(children(), opts)
end
children = [
supervisor(FarmbotCore.Logger.Repo, [])
]
def children do
default = [supervisor(FarmbotCore.Logger.Repo, [])]
config = Application.get_env(:farmbot_ext, __MODULE__) || []
Keyword.get(config, :children, default)
opts = [strategy: :one_for_all]
supervise(children, opts)
end
end

Some files were not shown because too many files have changed in this diff Show More