diff --git a/.gitignore b/.gitignore
index 935faf553..e558b0a0f 100755
--- a/.gitignore
+++ b/.gitignore
@@ -1,17 +1,18 @@
-**.orig
+_yardoc
+.bundle
+.config
+.DS_Store
+.rspec
+.rvmrc
+.vscode/
+.yardoc
*.gem
*.log
*.pem
*.rbc
*.rbc
+**.orig
*journal
-.DS_Store
-.bundle
-.config
-.rspec
-.rvmrc
-.vscode/
-.yardoc
/.bundle
/config/application.yml
/coverage/
@@ -24,14 +25,14 @@
/spec/tmp
/tmp
/vendor/bundle
-InstalledFiles
-_yardoc
api_docs.md
capybara-*.html
config/database.yml
coverage
doc/
erd.pdf
+InstalledFiles
+latest_corpus.ts
lib/bundler/man
log/*.log
node_modules/
@@ -47,4 +48,33 @@ spec/reports
test/tmp
test/version_tmp
tmp
-latest_corpus.ts
\ No newline at end of file
+
+# =====
+
+frontend/*.js.map
+frontend/.DS_Store
+frontend/.vscode
+frontend//src/config.json
+frontend/bower_components
+frontend/build
+frontend/coverage
+frontend/dist
+frontend/jest
+frontend/.nvmrc
+frontend/node_modules
+frontend/bundle.*.js*
+frontend/front_page.*.js*
+frontend/verify.*.js*
+frontend/public/app-index.js
+frontend/public/app-resources/*.css*
+frontend/public/app-resources/*.js*
+frontend/public/app-resources/chunks/*.js*
+frontend/public/app/*
+frontend/public/front_page.js
+frontend/public/index.html
+frontend/public/password_reset.html
+frontend/public/tos_update.html
+frontend/public/verify.html
+frontend/public/*.eot
+frontend/*.log
+frontend/*.log.*
diff --git a/frontend/CONTRIBUTING.md b/frontend/CONTRIBUTING.md
new file mode 100644
index 000000000..aed84960d
--- /dev/null
+++ b/frontend/CONTRIBUTING.md
@@ -0,0 +1 @@
+To get started, sign the Contributor License Agreement.
diff --git a/frontend/ISSUE_TEMPLATE b/frontend/ISSUE_TEMPLATE
new file mode 100644
index 000000000..d341d4f51
--- /dev/null
+++ b/frontend/ISSUE_TEMPLATE
@@ -0,0 +1,5 @@
+# Expected Behavior
+
+# Actual Behavior
+
+# Steps to Reproduce
diff --git a/frontend/LICENSE b/frontend/LICENSE
new file mode 100644
index 000000000..689aa206c
--- /dev/null
+++ b/frontend/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Farmbot.io
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 000000000..aa83ecd8e
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,63 @@
+[![Build Status](https://travis-ci.org/FarmBot/farmbot-web-frontend.svg?branch=master)](https://travis-ci.org/FarmBot/farmbot-web-frontend)
+[![codebeat badge](https://codebeat.co/badges/73a8b8b6-2683-4bea-a759-e3a07210e4ca)](https://codebeat.co/projects/github-com-rickcarlino-farmbot-web-frontend-master)
+[![Coverage Status](https://coveralls.io/repos/github/FarmBot/farmbot-web-frontend/badge.svg?branch=master)](https://coveralls.io/github/FarmBot/farmbot-web-frontend?branch=master) (we're working on it)
+# Do I need this?
+
+This repository is intended for *software developers* who wish to modify the frontend of the FarmBot Web App or host it on their own server. **If you are not a developer**, you are highly encouraged to use the free hosted web app at [my.farmbot.io](http://my.farmbot.io/).
+
+If you would like to report a problem with the web app, please [submit an issue](https://github.com/FarmBot/farmbot-web-frontend/issues/new).
+
+# FarmBot Web Frontend
+
+ This is the Javascript / HTML / CSS of the FarmBot web app. It depends on a [backend API](https://github.com/FarmBot/Farmbot-Web-API) (my.farmbot.io by default).
+
+# Developer Setup
+
+**[LATEST STABLE VERSION IS HERE](https://github.com/FarmBot/farmbot-web-frontend/releases)** :star: :star: :star:
+
+0. [Install node](https://nodejs.org/en/download/) if you haven't already.
+1. [Install Google Chrome](https://www.google.com/chrome/) for best app experience.
+2. `git clone https://github.com/FarmBot/farmbot-web-frontend.git`
+3. `cd farmbot-web-frontend`
+4. `npm install`
+5. `npm start`
+6. Visit `http://localhost:8080/`
+
+# Deploy to Production
+
+**NOTE:** The [Web API](https://github.com/FarmBot/Farmbot-Web-API) deployment will automatically build the latest version of the frontend and mount it in the web server. The instructions below are intended for reference purposes, or for users who wish to host their frontend code on a different server than their API.
+
+1. (optional, usually not needed) If you have an NPM module that needs to get baked into the build, pass the NPM modules name in as `NPM_ADDON=foo`
+2. run `npm run build`
+3. Copy the contents of `/app` into your webserver and it will be accessible via `/`.
+4. Visit `/` on your web server to verify installation.
+5. [Submit an issue](https://github.com/FarmBot/farmbot-web-frontend/issues/new?title=Installation%20Failure) if you hit problems during the installation.
+
+# Debugging external devices (DEV ONLY)
+
+[Weinre](https://www.npmjs.com/package/weinre) is included in this project.
+To utilize it, head over to the `/src` directory of the app, add a file called
+`config.json`, and populate it with this:
+```
+{
+ "ip_address": "YOUR-IP-ADDRESS"
+}
+```
+Then, in your console, `weinre --boundHost YOUR-IP-ADDRESS --httpPort 8081`.
+This should run in tandem with the rest of your project.
+Then navigate to http://YOUR-IP-ADDRESS:8081/client/#anonymous.
+After adding the `config.json`, you may be required to `npm start` again.
+
+# Want to Help?
+
+Check out the [Low Hanging Fruit](https://github.com/FarmBot/farmbot-web-frontend/search?l=typescript&q=TODO&utf8=%E2%9C%93).
+
+Also, if you're experiencing UI/UX issues, please include any possible specifications (device type, device OS, and device browser) to help in the debugging process. Bonus points for GIFs and screenshots. :fist:
+
+# Translating the app into your language
+Thanks for your interest in internationalizing the FarmBot web app! To add translations:
+
+1. Fork this repo
+2. Create a `yy.js` file in ``/public/app-resources/languages/`` where `yy` is your language's [language code](http://www.science.co.il/Language/Locale-codes.php). Eg: `ru` for Russian. If your language already has a file, then you can skip this step.
+3. Search the application for calls to `t()`. Any file that imports `from "i18next"` will have strings that require translation.
+4. When you have updated or added new translations, commit/push your changes and submit a pull request.
diff --git a/frontend/favicon.ico b/frontend/favicon.ico
new file mode 100644
index 000000000..413744015
Binary files /dev/null and b/frontend/favicon.ico differ
diff --git a/frontend/notes.md b/frontend/notes.md
new file mode 100644
index 000000000..4d38ca73b
--- /dev/null
+++ b/frontend/notes.md
@@ -0,0 +1,23 @@
+# Encoder Scaling
+|GCode|Param|Name|
+|---|---|---|---|
+|F15 |115|Enc. scaling|
+|F16 |116|Enc. scaling|
+|F17?|117|Enc. scaling|
+|F05?|105|Enc. type|
+|F06?|106|Enc. type|
+|F07?|107|Enc. type|
+|F22 (write 36)|Add to FarmBot JS|Enable X2|
+|F22 (write 37)|Add to FarmBot JS|Invert X2|
+|????|???|Set Home X|
+|????|???|Set Home Y|
+|????|???|Set Home Z|
+
+# MCU Reset button
+
+```
+{
+kind: "factory_reset"
+args: {package: "arduino_firmware" || "farmbot_os"}
+}
+```
diff --git a/frontend/notes.tsx b/frontend/notes.tsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 000000000..394153d1a
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,118 @@
+{
+ "name": "farmbot-web-frontend",
+ "version": "1.1.0",
+ "description": "Farmbot web frontend.",
+ "main": "dist/entry.js",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/farmbot/farmbot-web-frontend"
+ },
+ "scripts": {
+ "coverage": "midori coverage/remapped/html/index.html",
+ "build": "node_modules/webpack/bin/webpack.js --config tools/webpack.config.prd.js --display-error-details",
+ "start": "webpack-dev-server --config tools/webpack.config.dev.js --content-base public/ --host 0.0.0.0",
+ "test": "jest --coverage --no-cache && cat ./coverage/remapped/lcov.info | ./node_modules/coveralls/bin/coveralls.js",
+ "just_test": "jest --coverage --no-cache"
+ },
+ "keywords": [
+ "farmbot"
+ ],
+ "author": "farmbot.io",
+ "license": "MIT",
+ "optionalDependencies": {
+ "webpack-dev-server": "^1.14.1"
+ },
+ "dependencies": {
+ "@blueprintjs/core": "^1.20.0",
+ "@blueprintjs/labs": "^0.1.0",
+ "@types/deep-freeze": "^0.1.0",
+ "@types/enzyme": "^2.7.8",
+ "@types/fastclick": "^1.0.28",
+ "@types/handlebars": "^4.0.31",
+ "@types/history": "^2.0.39",
+ "@types/i18next": "^2.3.32",
+ "@types/jest": "^19.2.2",
+ "@types/lodash": "^4.14.64",
+ "@types/markdown-it": "0.0.1",
+ "@types/mqtt": "0.0.32",
+ "@types/node": "^6.0.63",
+ "@types/react": "^0.14.57",
+ "@types/react-color": "^2.11.0",
+ "@types/react-dom": "^0.14.18",
+ "@types/react-redux": "^4.4.32",
+ "@types/react-router": "^3.0.0",
+ "@types/redux": "^3.6.31",
+ "axios": "^0.14.0",
+ "boxed_value": "^1.0.0",
+ "coveralls": "^2.13.0",
+ "css-loader": "^0.25.0",
+ "deep-freeze": "^0.0.1",
+ "enzyme": "^2.8.1",
+ "extract-text-webpack-plugin": "^2.0.0-beta.5",
+ "farmbot": "4.0.7",
+ "farmbot-toastr": "^1.0.2",
+ "fastclick": "^1.0.6",
+ "file-loader": "^0.10.0",
+ "handlebars": "^4.0.5",
+ "i18next": "^3.4.3",
+ "imports-loader": "^0.7.0",
+ "jest": "^19.0.2",
+ "json-loader": "^0.5.4",
+ "lodash": "^3.10.1",
+ "markdown-it": "^8.2.1",
+ "markdown-it-emoji": "^1.3.0",
+ "moment": "2.15.2",
+ "node-sass": "^3.10.0",
+ "optimize-css-assets-webpack-plugin": "^1.3.0",
+ "react": "^15.5.4",
+ "react-addons-css-transition-group": "^15.6.0",
+ "react-addons-test-utils": "^15.5.1",
+ "react-color": "^2.11.1",
+ "react-dom": "^15.5.4",
+ "react-redux": "^4.4.1",
+ "react-router": "^3.0.0",
+ "react-test-renderer": "^15.5.4",
+ "redux": "^3.3.1",
+ "redux-immutable-state-invariant": "^1.2.3",
+ "redux-thunk": "^2.0.1",
+ "sass-loader": "^4.0.2",
+ "style-loader": "^0.13.0",
+ "ts-jest": "^19.0.9",
+ "ts-loader": "^1.0.0",
+ "tslint": "4.5.1",
+ "typescript": "2.3",
+ "url-loader": "^0.5.7",
+ "webpack": "^2.2.0-rc.3",
+ "webpack-uglify-js-plugin": "^1.1.9",
+ "weinre": "^2.0.0-pre-I0Z7U9OV",
+ "yarn": "^0.23.4"
+ },
+ "devDependencies": {
+ "jscpd": "^0.6.10",
+ "webpack-notifier": "^1.5.0"
+ },
+ "jest": {
+ "setupFiles": [
+ "./src/unmock_i18next.ts",
+ "./src/__test_support__/locastorage.js"
+ ],
+ "transform": {
+ ".(ts|tsx)": "/node_modules/ts-jest/preprocessor.js"
+ },
+ "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$",
+ "moduleFileExtensions": [
+ "ts",
+ "tsx",
+ "js"
+ ],
+ "testResultsProcessor": "/node_modules/ts-jest/coverageprocessor.js",
+ "collectCoverage": true,
+ "collectCoverageFrom": [
+ "src/**/*.{ts,tsx}"
+ ],
+ "coverageReporters": [
+ "html",
+ "json"
+ ]
+ }
+}
diff --git a/frontend/public/404.html b/frontend/public/404.html
new file mode 100755
index 000000000..c4c127dea
--- /dev/null
+++ b/frontend/public/404.html
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+ The page you are looking for does not exist.
+ Try going back.
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/public/422.html b/frontend/public/422.html
new file mode 100755
index 000000000..fbb4b84d7
--- /dev/null
+++ b/frontend/public/422.html
@@ -0,0 +1,58 @@
+
+
+
+ The change you wanted was rejected (422)
+
+
+
+
+
+
+
The change you wanted was rejected.
+
Maybe you tried to change something you didn't have access to.
+
+
If you are the application owner check the logs for more information.
;
+ }
+}
diff --git a/frontend/src/auth/actions.ts b/frontend/src/auth/actions.ts
new file mode 100644
index 000000000..179ca918b
--- /dev/null
+++ b/frontend/src/auth/actions.ts
@@ -0,0 +1,136 @@
+import * as Axios from "axios";
+import { t } from "i18next";
+import { error, success } from "farmbot-toastr";
+import { connectDevice, fetchReleases } from "../devices/actions";
+import { push } from "../history";
+import { AuthState } from "./interfaces";
+import { ReduxAction, Thunk } from "../redux/interfaces";
+import * as Sync from "../sync/actions";
+import { API } from "../api";
+import { toastErrors } from "../util";
+import { Session } from "../session";
+import { UnsafeError } from "../interfaces";
+import {
+ responseFulfilled,
+ responseRejected,
+ requestFulfilled
+} from "../interceptors";
+import { Actions } from "../constants";
+
+export function didLogin(authState: AuthState, dispatch: Function) {
+ API.setBaseUrl(authState.token.unencoded.iss);
+ dispatch(fetchReleases(authState.token.unencoded.os_update_server));
+ dispatch(loginOk(authState));
+
+ Sync.fetchSyncData(dispatch);
+ dispatch(connectDevice(authState.token.encoded));
+};
+
+// We need to handle OK logins for numerous use cases (Ex: login & registration)
+function onLogin(dispatch: Function) {
+ return (response: Axios.AxiosXHR) => {
+ let { data } = response;
+ Session.put(data);
+ didLogin(data, dispatch);
+ push("/app/controls");
+ };
+};
+
+export function login(username: string, password: string, url: string): Thunk {
+ return dispatch => {
+ return requestToken(username, password, url).then(
+ onLogin(dispatch),
+ (err) => dispatch(loginErr())
+ );
+ };
+}
+
+function loginErr() {
+ error(t("Login failed."));
+ return { type: "LOGIN_ERR" };
+}
+
+/** Very important. Once called, all outbound HTTP requests will
+ * have a JSON Web Token attached to their "Authorization" header,
+ * thereby granting access to the API. */
+export function loginOk(auth: AuthState): ReduxAction {
+ Axios.interceptors.response.use(responseFulfilled, responseRejected);
+ Axios.interceptors.request.use(requestFulfilled(auth));
+
+ return {
+ type: Actions.LOGIN_OK,
+ payload: auth
+ };
+}
+
+/** Sign up for the FarmBot service over AJAX. */
+export function register(name: string,
+ email: string,
+ password: string,
+ confirmation: string,
+ url: string): Thunk {
+ return dispatch => {
+ let p = requestRegistration(name,
+ email,
+ password,
+ confirmation,
+ url);
+ return p.then(onLogin(dispatch),
+ onRegistrationErr(dispatch));
+ };
+}
+
+/** Handle user registration errors. */
+export function onRegistrationErr(dispatch: Function) {
+ return (err: UnsafeError) => {
+ toastErrors(err);
+ dispatch({
+ type: "REGISTRATION_ERROR",
+ payload: err
+ });
+ };
+}
+
+/** Build a JSON object in preparation for an HTTP POST
+ * to registration endpoint */
+function requestRegistration(name: string,
+ email: string,
+ password: string,
+ confirmation: string,
+ url: string) {
+ let form = {
+ user: {
+ email: email,
+ password: password,
+ password_confirmation: confirmation,
+ name: name
+ }
+ };
+ return Axios.post(API.current.usersPath, form);
+}
+
+/** Fetch API token if already registered. */
+function requestToken(email: string,
+ password: string,
+ url: string) {
+ let payload = { user: { email: email, password: password } };
+ // Set the base URL once here.
+ // It will get set once more when we get the "iss" claim from the JWT.
+ API.setBaseUrl(url);
+ return Axios.post(API.current.tokensPath, payload);
+}
+
+export function logout() {
+ // When logging out, we pop up a toast message to confirm logout.
+ // Sometimes, LOGOUT is dispatched when the user is already logged out.
+ // In those cases, seeing a logout message may confuse the user.
+ // To circumvent this, we must check if the user had a token.
+ // If there was infact a token, we can safely show the message.
+ if (Session.get()) { success("You have been logged out."); }
+ Session.clear(true);
+ // Technically this is unreachable code:
+ return {
+ type: "LOGOUT",
+ payload: {}
+ };
+}
diff --git a/frontend/src/auth/interfaces.ts b/frontend/src/auth/interfaces.ts
new file mode 100644
index 000000000..e06e6eec7
--- /dev/null
+++ b/frontend/src/auth/interfaces.ts
@@ -0,0 +1,38 @@
+export interface Token {
+ unencoded: UnencodedToken;
+ encoded: string;
+}
+
+export interface AuthState {
+ token: Token;
+}
+
+export interface UnencodedToken {
+ // /** SUBJECT - The user's email. STOP USING THIS! */
+ // sub: string;
+ /** ISSUED AT */
+ iat: number;
+ /** JSON TOKEN IDENTIFIER - a serial number for the token. */
+ jti: string;
+ /** ISSUER - Where token came from (API URL). */
+ iss: string;
+ /** EXPIRATION DATE */
+ exp: number;
+ /** MQTT server address */
+ mqtt: string;
+ /** BOT UNIQUE IDENTIFIER */
+ bot: string;
+ /** Where to download RPi software */
+ os_update_server: string;
+ /** Where to download firmware. */
+ fw_update_server: string;
+}
+
+export interface User {
+ id: number;
+ device_id: number;
+ name: string;
+ email: string;
+ created_at: string;
+ updated_at: string;
+}
diff --git a/frontend/src/auth/reducer.ts b/frontend/src/auth/reducer.ts
new file mode 100644
index 000000000..864e0f277
--- /dev/null
+++ b/frontend/src/auth/reducer.ts
@@ -0,0 +1,8 @@
+import { AuthState } from "./interfaces";
+import { generateReducer } from "../redux/generate_reducer";
+import { Actions } from "../constants";
+
+export let authReducer = generateReducer(undefined)
+ .add(Actions.LOGIN_OK, (s, { payload }) => {
+ return payload;
+ });
diff --git a/frontend/src/config/__tests__/actions_test.ts b/frontend/src/config/__tests__/actions_test.ts
new file mode 100644
index 000000000..1e00672fc
--- /dev/null
+++ b/frontend/src/config/__tests__/actions_test.ts
@@ -0,0 +1,17 @@
+jest.unmock("../../auth/actions");
+const actions = require("../../auth/actions");
+let didLogin = jest.fn();
+jest.mock("../../session", () => ({ Session: { get: () => false } }));
+actions.didLogin = didLogin;
+import { ready } from "../actions";
+
+const STUB_STATE = { auth: "FOO BAR BAZ" };
+describe("Actions", () => {
+ it("fetches configs and calls didLogin()", () => {
+ let dispatch = jest.fn();
+ let getState = jest.fn(() => STUB_STATE);
+ let thunk = ready();
+ thunk(dispatch, getState);
+ expect(didLogin.mock.calls.length).toBe(1);
+ });
+});
diff --git a/frontend/src/config/actions.ts b/frontend/src/config/actions.ts
new file mode 100644
index 000000000..ef346e2c9
--- /dev/null
+++ b/frontend/src/config/actions.ts
@@ -0,0 +1,11 @@
+import { didLogin } from "../auth/actions";
+import { Thunk } from "../redux/interfaces";
+import { Session } from "../session";
+
+/** Lets Redux know that the app is ready to bootstrap. */
+export function ready(): Thunk {
+ return (dispatch, getState) => {
+ let state = Session.get() || getState().auth;
+ if (state) { didLogin(state, dispatch); };
+ };
+}
diff --git a/frontend/src/config/interfaces.ts b/frontend/src/config/interfaces.ts
new file mode 100644
index 000000000..00baf6247
--- /dev/null
+++ b/frontend/src/config/interfaces.ts
@@ -0,0 +1,11 @@
+/** Payload of CHANGE_API_HOST */
+export interface ChangeApiHost { host: string; };
+
+/** Payload of CHANGE_API_PORT */
+export interface ChangeApiPort { port: string; };
+
+/** This is a subset of attributes found on window.location. */
+export interface ConfigState {
+ host: string;
+ port: string;
+}
diff --git a/frontend/src/config/reducer.ts b/frontend/src/config/reducer.ts
new file mode 100644
index 000000000..4341216fc
--- /dev/null
+++ b/frontend/src/config/reducer.ts
@@ -0,0 +1,22 @@
+import { generateReducer } from "../redux/generate_reducer";
+import { ChangeApiHost, ChangeApiPort, ConfigState } from "./interfaces";
+import { API } from "../api";
+import { Actions } from "../constants";
+
+let initialState: ConfigState = {
+ host: location.hostname,
+ // It gets annoying to manually change the port # in dev mode.
+ // I automatically point to port 3000 on local.
+ port: API.inferPort()
+};
+
+export let configReducer = generateReducer(initialState)
+ .add(Actions.CHANGE_API_PORT, (s, { payload }) => {
+ s.port = payload.port.replace(/\D/g, "");
+ return s;
+ })
+ .add(Actions.CHANGE_API_HOST, (s, { payload }) => {
+ s.host = payload.host;
+ return s;
+ });
+
diff --git a/frontend/src/constants.ts b/frontend/src/constants.ts
new file mode 100644
index 000000000..a1048259d
--- /dev/null
+++ b/frontend/src/constants.ts
@@ -0,0 +1,321 @@
+/**
+ * Seems like a better idea to keep content and tooltips centralized. If we have
+ * the ability to keep the app safer from possible accidental breakages by
+ * avoiding going into components for copy changes, why not right? ¯\_(ツ)_/¯
+ */
+
+export namespace ToolTips {
+
+ // Controls
+ export const MOVE =
+ `Use these manual control buttons to move FarmBot in realtime. Press the
+ arrows for relative movements or type in new coordinates and press GO for an
+ absolute movement. Tip: Press the Home button when you are done so FarmBot
+ is ready to get back to work.`
+
+ export const WEBCAM_SAVE =
+ `Press the edit button to update and save your webcam URL.`
+
+ export const PERIPHERALS =
+ `Use these toggle switches to control FarmBot's peripherals in realtime. To
+ edit and create new peripherals, press the EDIT button. Make sure to turn
+ things off when you're done!`
+
+ // Device
+ export const OS_SETTINGS =
+ `View and change device settings.`
+
+ export const HW_SETTINGS =
+ `Change settings of your FarmBot hardware with the fields below. Caution:
+ Changing these settings to extreme values can cause hardware malfunction.
+ Make sure to test any new settings before letting your FarmBot use them
+ unsupervised. Tip: Recalibrate FarmBot after changing settings and test a
+ few sequences to verify that everything works as expected. Note: Currently
+ not all settings can be changed.`
+
+ // Hardware Settings: Homing and Calibration
+ export const HOMING =
+ `(Alpha) If encoders or end-stops are enabled, home axis (find zero).`
+
+ export const CALIBRATION =
+ `(Alpha) If encoders or end-stops are enabled, home axis and determine
+ maximum.`
+
+ export const SET_ZERO_POSITION =
+ `Set the current location as zero.`
+
+ export const FIND_HOME_ON_BOOT =
+ `If encoders or end-stops are enabled, find the home position when the
+ device powers on.`
+
+ export const STOP_AT_HOME =
+ `Stop at the home location of the axis.`
+
+ export const STOP_AT_MAX =
+ `Don't allow movement past the maximum value provided in AXIS LENGTH.`
+
+ export const NEGATIVE_COORDINATES_ONLY =
+ `Restrict travel to negative coordinate locations. Overridden by disabling
+ STOP AT HOME.`
+
+ export const LENGTH =
+ `Set the length of each axis to provide software limits. Used only if
+ STOP AT MAX is enabled.`
+
+ export const TIMEOUT_AFTER =
+ `Amount of time to wait for a command to execute before stopping.`
+
+ // Hardware Settings: Motors
+ export const MAX_MOVEMENT_RETRIES =
+ `Number of times to retry a movement before stopping.`
+
+ export const MAX_SPEED =
+ `Maximum travel speed after acceleration in motor steps per second.`
+
+ export const MIN_SPEED =
+ `Minimum movement speed. Also used for homing, calibration, and movements
+ across home.`
+
+ export const ACCELERATE_FOR =
+ `Number of steps used for acceleration and deceleration.`
+
+ export const STEPS_PER_MM =
+ `The number of motor steps required to move the axis one millimeter.`
+
+ export const ALWAYS_POWER_MOTORS =
+ `Keep power applied to motors. Prevents slipping from gravity in certain
+ situations.`
+
+ export const INVERT_MOTORS =
+ `Invert direction of motor during calibration.`
+
+ export const ENABLE_X2_MOTOR =
+ `Enable use of a second x-axis motor. Connects to E0 on RAMPS.`
+
+ // Hardware Settings: Encoders and Endstops
+ export const ENABLE_ENCODERS =
+ `(Alpha) Enable use of rotary encoders during calibration and homing.`
+
+ export const ENCODER_POSITIONING =
+ `[EXPERIMENTAL] Use encoders for positioning.`
+
+ export const INVERT_ENCODERS =
+ `(Alpha) Reverse the direction of encoder position reading.`
+
+ export const MAX_MISSED_STEPS =
+ `(Alpha) Number of steps missed (determined by encoder) before motor is
+ considered to have stalled.`
+
+ export const ENCODER_MISSED_STEP_DECAY =
+ `(Alpha) Reduction to missed step total for every good step.`
+
+ export const ENCODER_SCALING =
+ `(Alpha) encoder scaling factor = 100 * (motor resolution * microsteps) /
+ (encoder resolution)`
+
+ export const ENABLE_ENDSTOPS =
+ `Enable use of electronic end-stops during calibration and homing.`
+
+ export const INVERT_ENDPOINTS =
+ `Swap axis end-stops during calibration.`
+
+ // Farmware
+ export const FARMWARE =
+ `Manage Farmware (plugins).`
+
+ export const PHOTOS =
+ `Take and view photos with your FarmBot's camera.`
+
+ export const WEED_DETECTOR =
+ `Detect weeds using FarmBot's camera and display them on the Farm Designer
+ map.`
+
+ export const CAMERA_CALIBRATION =
+ `Calibrate FarmBot's camera for use in the weed detection software.`
+
+ // Sequences
+ export const SEQUENCE_COMMANDS =
+ `These are the most basic commands FarmBot can execute. Drag and drop them
+ to create sequences for watering, planting seeds, measuring soil properties,
+ and more.`
+
+ export const SEQUENCE_EDITOR =
+ `Drag and drop commands here to create sequences for watering, planting
+ seeds, measuring soil properties, and more. Press the Test button to
+ immediately try your sequence with FarmBot. You can also edit, copy, and
+ delete existing sequences; assign a color; and give your commands custom
+ names.`
+
+ export const SEQUENCE_LIST =
+ `Here is the list of all of your sequences. Click one to edit.`
+
+ export const MOVE_ABSOLUTE =
+ `The Move Absolute step instructs FarmBot to move to the specified
+ coordinate regardless of the current position. For example, if FarmBot is
+ currently at X=1000, Y=1000 and it receives a Move Absolute where X=0 and
+ Y=3000, then FarmBot will move to X=0, Y=3000. If FarmBot must move in
+ multiple directions, it will move diagonally. If you require straight
+ movements along one axis at a time, use multiple Move Absolute steps.
+ Offsets allow you to more easily instruct FarmBot to move to a location,
+ but offset from it by the specified amount. For example moving to just
+ above where a peripheral is located. Using offsets lets FarmBot do the
+ math for you.`
+
+ export const MOVE_RELATIVE =
+ `The Move Relative step instructs FarmBot to move the specified distance
+ from its current location. For example, if FarmBot is currently at X=1000,
+ Y=1000 and it receives a Move Relative where X=0 and Y=3000, then FarmBot
+ will move to X=1000, Y=4000. If FarmBot must move in multiple directions,
+ it will move diagonally. If you require straight movements along one axis
+ at a time, use multiple Move Relative steps. Move Relative steps should be
+ preceded by a Move Absolute step to ensure you are starting from a known
+ location.`
+
+ export const WRITE_PIN =
+ `The Write Pin step instructs FarmBot to set the specified pin on the
+ Arduino to the specified mode and value. A Pin Mode of 0 is for on/off
+ control, while a Pin Mode of 1 is for PWM (pulse width modulation) (0-255).`
+
+ export const READ_PIN =
+ `The Read Pin step instructs FarmBot to read the current value of the
+ specified pin. A Pin Mode of 0 is for digital (on/off), while a Pin Mode
+ of 1 is for analog (0-1023 for 0-5V).`
+
+ export const WAIT =
+ `The Wait step instructs FarmBot to wait for the specified amount of time.
+ Use it in combination with the Pin Write step to water for a length of
+ time.`
+
+ export const SEND_MESSAGE =
+ `The Send Message step instructs FarmBot to send a custom message to the
+ logs (and toast message and/or email, if selected). This can help you with
+ debugging your sequences.`
+
+ export const FIND_HOME =
+ `The Find Home step instructs the device to perform a homing command to
+ find and set zero for the chosen axis or axes.`
+
+ export const IF =
+ `Execute a sequence if a condition is satisfied. If the condition is not
+ satisfied, chose to do nothing or execute a different sequence.`
+
+ export const EXECUTE_SCRIPT =
+ `The Run Farmware step runs a Farmware package. The weed detection script
+ is the only script supported at the moment, but user definable script
+ support is coming soon!`
+
+ export const TAKE_PHOTO =
+ `Snaps a photo using the device camera. Select the camera type on the
+ Device page.`
+
+ // Regimens
+ export const BULK_SCHEDULER =
+ `Add sequences to your regimen by selecting a sequence from the drop down,
+ specifying a time, choosing which days it should run on, and then clicking
+ the + button. For example: a Seeding sequence might be scheduled for Day 1,
+ while a Watering sequence would be scheduled to run every other day.`
+
+ export const REGIMEN_EDITOR =
+ `Regimens allow FarmBot to take care of a plant throughout its entire life.
+ A regimen consists of many sequences that are scheduled to run based on the
+ age of the plant. Regimens are applied to plants from the farm designer
+ (coming soon) and can be re-used on many plants growing at the same or
+ different times. Multiple regimens can be applied to any one plant.`
+
+ export const REGIMEN_LIST =
+ `This is a list of all of your regimens. Click one to begin editing it.`
+
+ // Tools
+ export const TOOL_LIST =
+ `This is a list of all your FarmBot Tools. Click the Edit button to add,
+ edit, or delete tools.`
+
+ export const TOOLBAY_LIST =
+ `Toolbays are where you store your FarmBot Tools. Each Toolbay has Slots
+ that you can put your Tools in, which should be reflective of your real
+ FarmBot hardware configuration.`
+
+}
+
+export namespace Content {
+
+ // Account
+ export const ACCOUNT_DELETE_WARNING =
+ `WARNING! Deleting your account will permanently delete all of your
+ Sequences , Regimens, Events, and Farm Designer data.Upon deleting your
+ account, FarmBot will cease to function and become inaccessible until it is
+ paired with another web app account. To do this, you will need to reboot
+ your FarmBot so that is goes back into configuration mode for pairing with
+ another user account. When this happens, all of the data on your FarmBot
+ will be overwritten with the new account's data. If the account is brand
+ new, then FarmBot will become a blank slate.`
+
+ // Controls
+ export const FACTORY_RESET_WARNING =
+ `Factory resetting your FarmBot will destroy all data on the device,
+ revoking your FarmBot's abilily to connect to your web app account and your
+ home wifi. Upon factory resetting, your device will restart into
+ Configurator mode. Factory resetting your FarmBot will not affect any data
+ or settings from your web app account, allowing you to do a complete restore
+ to your device once it is back online and paired with your web app account.`
+
+}
+
+export namespace Actions {
+
+ // Resources
+ export const DESTROY_RESOURCE_OK = `DESTROY_RESOURCE_OK`
+ export const INIT_RESOURCE = `INIT_RESOURCE`
+ export const SAVE_SPECIAL_RESOURCE = `SAVE_SPECIAL_RESOURCE`
+ export const SAVE_RESOURCE_OK = `SAVE_RESOURCE_OK`
+ export const UPDATE_RESOURCE_OK = `UPDATE_RESOURCE_OK`
+ export const EDIT_RESOURCE = `EDIT_RESOURCE`
+ export const OVERWRITE_RESOURCE = `OVERWRITE_RESOURCE`
+ export const SAVE_RESOURCE_START = `SAVE_RESOURCE_START`
+ export const RESOURCE_READY = `RESOURCE_READY`
+
+ // Auth
+ export const LOGIN_OK = `LOGIN_OK`
+
+ // Config
+ export const CHANGE_API_PORT = `CHANGE_API_PORT`
+ export const CHANGE_API_HOST = `CHANGE_API_HOST`
+
+ // Devices
+ export const TOGGLE_CONTROL_PANEL_OPTION = `TOGGLE_CONTROL_PANEL_OPTION`
+ export const CHANGE_STEP_SIZE = `CHANGE_STEP_SIZE`
+ export const SETTING_UPDATE_START = `SETTING_UPDATE_START`
+ export const SETTING_UPDATE_END = `SETTING_UPDATE_END`
+ export const BOT_CHANGE = `BOT_CHANGE`
+ export const FETCH_OS_UPDATE_INFO_OK = `FETCH_OS_UPDATE_INFO_OK`
+ export const FETCH_FW_UPDATE_INFO_OK = `FETCH_FW_UPDATE_INFO_OK`
+ export const SET_SYNC_STATUS = `SET_SYNC_STATUS`
+ export const INVERT_JOG_BUTTON = `INVERT_JOG_BUTTON`
+
+ // Draggable
+ export const PUT_DATA_XFER = `PUT_DATA_XFER`
+ export const DROP_DATA_XFER = `DROP_DATA_XFER`
+
+ // Designer
+ export const SEARCH_QUERY_CHANGE = `SEARCH_QUERY_CHANGE`
+ export const SELECT_PLANT = `SELECT_PLANT`
+ export const TOGGLE_HOVERED_PLANT = `TOGGLE_HOVERED_PLANT`
+ export const UPDATE_BOT_ORIGIN_QUADRANT = `UPDATE_BOT_ORIGIN_QUADRANT`
+ export const UPDATE_MAP_ZOOM_LEVEL = `UPDATE_MAP_ZOOM_LEVEL`
+ export const OF_SEARCH_RESULTS_OK = `OF_SEARCH_RESULTS_OK`
+
+ // Regimens
+ export const PUSH_WEEK = `PUSH_WEEK`
+ export const POP_WEEK = `POP_WEEK`
+ export const TOGGLE_DAY = `TOGGLE_DAY`
+ export const SELECT_REGIMEN = `SELECT_REGIMEN`
+ export const SET_SEQUENCE = `SET_SEQUENCE`
+ export const SET_TIME_OFFSET = `SET_TIME_OFFSET`
+
+ // Sequences
+ export const SELECT_SEQUENCE = `SELECT_SEQUENCE`
+
+ // Farmware
+ export const SELECT_IMAGE = `SELECT_IMAGE`
+
+}
diff --git a/frontend/src/controls/axis_input_box.tsx b/frontend/src/controls/axis_input_box.tsx
new file mode 100644
index 000000000..c0178223a
--- /dev/null
+++ b/frontend/src/controls/axis_input_box.tsx
@@ -0,0 +1,67 @@
+import * as React from "react";
+import { isNaN } from "lodash";
+import { AxisInputBoxProps, AxisInputBoxState } from "./interfaces";
+
+export class AxisInputBox
+ extends React.Component {
+ constructor() {
+ super();
+ this.state = { value: undefined };
+ }
+
+ whatToDisplay() {
+ if (this.state.value === undefined) {
+ return this.props.value;
+ } else {
+ return this.state.value;
+ }
+ }
+
+ style() {
+ let border = "1px solid red";
+ return (this.state.value === undefined) ? {} : { border };
+ }
+
+ componentWillReceiveProps(nextProps: AxisInputBoxProps) {
+ if (this.props.value !== nextProps.value) {
+ this.reset();
+ }
+ }
+
+ blur = (e: React.FormEvent) => {
+ switch (this.state.value) {
+ case undefined:
+ return;
+ case "":
+ return this.reset();
+ default:
+ let num = parseFloat(this.state.value);
+ if (isNaN(num)) {
+ return this.reset();
+ } else {
+ return this.props.onChange(this.props.axis, num);
+ }
+ }
+ }
+
+ reset() {
+ this.setState({ value: undefined });
+ this.props.onChange(this.props.axis, undefined);
+ }
+
+ change = (e: React.FormEvent) => {
+ this.setState({ value: e.currentTarget.value });
+ }
+
+ render() {
+ return
+
+
+
;
+ }
+}
diff --git a/frontend/src/controls/axis_input_box_group.tsx b/frontend/src/controls/axis_input_box_group.tsx
new file mode 100644
index 000000000..063437e0d
--- /dev/null
+++ b/frontend/src/controls/axis_input_box_group.tsx
@@ -0,0 +1,66 @@
+import * as React from "react";
+import { AxisInputBox } from "./axis_input_box";
+import { t } from "i18next";
+import { Row } from "../ui";
+import {
+ AxisInputBoxGroupProps,
+ AxisInputBoxGroupState,
+ Vector
+} from "./interfaces";
+
+export class AxisInputBoxGroup extends React.Component> {
+ constructor() {
+ super();
+ this.state = {};
+ }
+
+ change = (axis: keyof Vector, val: number) => {
+ this.setState({ [axis]: val });
+ }
+
+ get vector() {
+ let { x, y, z } = this.state;
+ let [x2, y2, z2] = this.props.bot.hardware.location;
+
+ return {
+ x: _.isNumber(x) ? x : x2,
+ y: _.isNumber(y) ? y : y2,
+ z: _.isNumber(z) ? z : z2
+ };
+ }
+
+ clicked = () => {
+ this.props.onCommit(this.vector);
+ this.setState({ x: undefined, y: undefined, z: undefined });
+ }
+
+ render() {
+ let [x, y, z] = this.props.bot.hardware.location;
+ return
+
+
+
+
+
+
+ ;
+ }
+}
diff --git a/frontend/src/controls/controls.tsx b/frontend/src/controls/controls.tsx
new file mode 100644
index 000000000..fe621349f
--- /dev/null
+++ b/frontend/src/controls/controls.tsx
@@ -0,0 +1,118 @@
+import * as React from "react";
+import { connect } from "react-redux";
+import { t } from "i18next";
+import { changeStepSize, moveAbs } from "../devices/actions";
+import { Peripherals } from "./peripherals";
+import { EStopButton } from "../devices/components/e_stop_btn";
+import { JogButtons } from "./jog_buttons";
+import { AxisInputBoxGroup } from "./axis_input_box_group";
+import { Row, Page, Col, Widget, WidgetBody, WidgetHeader } from "../ui";
+import { mapStateToProps } from "./state_to_props";
+import { StepSizeSelector } from "./step_size_selector";
+import { MustBeOnline } from "../devices/must_be_online";
+import { ToolTips } from "../constants";
+import { WebcamPanel } from "./webcam_panel";
+import { Props } from "./interfaces";
+import { Xyz } from "../devices/interfaces";
+import { Popover, Position } from "@blueprintjs/core";
+
+@connect(mapStateToProps)
+export class Controls extends React.Component {
+
+ toggle = (name: Xyz) => () =>
+ this.props.dispatch({ type: "INVERT_JOG_BUTTON", payload: name });
+
+ render() {
+ let { sync_status } = this.props.bot.hardware.informational_settings;
+ let { x_axis_inverted, y_axis_inverted, z_axis_inverted } = this.props.bot;
+ let xBtnColor = x_axis_inverted ? "green" : "red";
+ let yBtnColor = y_axis_inverted ? "green" : "red";
+ let zBtnColor = z_axis_inverted ? "green" : "red";
+
+ return
+
+