Farmbot-Web-App/src/devices/actions.ts

396 lines
11 KiB
TypeScript

import { Farmbot } from "farmbot";
import { t } from "i18next";
import { get } from "axios";
import * as Axios from "axios";
import * as _ from "lodash";
import { success, warning, info, error } from "farmbot-toastr";
import { devices } from "../device";
import { Log } from "../interfaces";
import { GithubRelease, MoveRelProps } from "./interfaces";
import { Thunk, GetState } from "../redux/interfaces";
import { DeviceAccountSettings, BotState } from "../devices/interfaces";
import {
McuParams,
Configuration,
BotStateTree,
ALLOWED_CHANNEL_NAMES,
ALLOWED_MESSAGE_TYPES,
SyncStatus
} from "farmbot";
import { Sequence } from "../sequences/interfaces";
import { HardwareState, ControlPanelState } from "../devices/interfaces";
import { API } from "../api/index";
import { User } from "../auth/interfaces";
import { init, edit } from "../api/crud";
import { getDeviceAccountSettings } from "../resources/selectors";
import { TaggedDevice } from "../resources/tagged_resources";
import { versionOK } from "./reducer";
import { oneOf } from "../util";
const ON = 1, OFF = 0;
type configKey = keyof McuParams;
function incomingStatus(statusMessage: HardwareState) {
return { type: "BOT_CHANGE", payload: statusMessage };
}
function isLog(x: any): x is Log {
return _.isObject(x) && _.isString(x.message);
}
export function checkControllerUpdates() {
let noun = "Check for Updates";
devices
.current
.checkUpdates()
.then(commandOK(noun), commandErr(noun));
}
export function powerOff() {
let noun = "Power Off Bot";
devices
.current
.powerOff()
.then(commandOK(noun), commandErr(noun));
}
export function factoryReset() {
if (!confirm("WAIT! This will erase EVERYTHING stored on your device SD " +
"card. Are you sure?")) {
return;
}
devices
.current
.resetOS();
}
export function reboot() {
let noun = "Reboot Bot";
devices
.current
.reboot()
.then(commandOK(noun), commandErr(noun));
}
export function checkArduinoUpdates() {
let noun = "Check Firmware Updates";
devices
.current
.checkArduinoUpdates()
.then(commandOK(noun), commandErr(noun));
}
export function emergencyLock() {
let noun = "Emergency stop";
devices
.current
.emergencyLock()
.then(commandOK(noun), commandErr(noun));
}
const REBOOT_CONF = `Are you sure you want to unlock the device?
Device will reboot.`;
export function emergencyUnlock() {
let noun = "Emergency unlock";
if (confirm(REBOOT_CONF)) {
devices
.current
.reboot() // .emergencyUnlock is broke ATM RC 8 Jun 2017
.then(commandOK(noun), _.noop); // REMOVE NOOP WHEN YOU PUT BACK UNLOCK RC - June 8 2017
}
}
export function sync(): Thunk {
let noun = "Sync";
return function (dispatch, getState) {
let IS_OK = versionOK(getState()
.bot
.hardware
.informational_settings
.controller_version, 4, 0);
if (IS_OK) {
dispatch(setSyncStatus("syncing"));
devices
.current
.sync()
.then(() => {
commandOK(noun);
dispatch(setSyncStatus("synced"));
}).catch(() => {
commandErr(noun);
dispatch(setSyncStatus("sync_error"));
});
} else {
badVersion();
}
};
}
export function execSequence(sequence: Sequence) {
const noun = "Sequence execution";
if (sequence.id) {
return devices
.current
.execSequence(sequence.id)
.then(commandOK(noun), commandErr(noun));
} else {
throw new Error("Can't execute unsaved sequences");
}
}
export let saveAccountChanges: Thunk = function (dispatch, getState) {
return save(getDeviceAccountSettings(getState().resources.index));
};
let commandErr = (noun = "Command") => () => {
let msg = noun + " request failed.";
console.info("Took longer than 6 seconds: " + noun);
};
let commandOK = (noun = "Command") => () => {
let msg = noun + " request sent to device.";
success(msg, t("Request sent"));
};
export let fetchReleases =
(url: string) => (dispatch: Function, getState: Function) => {
get<GithubRelease>(url)
.then((resp) => {
let version = resp.data.tag_name;
let versionWithoutV = version.slice(1, version.length);
dispatch({
type: "FETCH_OS_UPDATE_INFO_OK",
payload: versionWithoutV
});
})
.catch((ferror) => {
error(t("Could not download firmware update information."));
dispatch({
type: "FETCH_OS_UPDATE_INFO_ERROR",
payload: ferror
});
});
};
export function save(input: TaggedDevice) {
return function (dispatch: Function, getState: GetState) {
return Axios
.put<User>(API.current.devicePath, input.body)
.then(resp => dispatch({ type: "SAVE_DEVICE_OK", payload: resp.data }))
.catch(resp => error("Error saving device settings."));
};
}
/**
* Toggles visibility of individual sections in the giant controls panel
* found on the Devices page.
*/
export function toggleControlPanel(payload: keyof ControlPanelState) {
return { type: "TOGGLE_CONTROL_PANEL_OPTION", payload };
}
export function changeDevice(device: TaggedDevice,
update: Partial<DeviceAccountSettings>): Thunk {
return function (dispatch, getState) {
dispatch(edit(device, update));
dispatch(save(getDeviceAccountSettings(getState().resources.index)));
};
}
export function MCUFactoryReset() {
return devices.current.resetMCU();
}
export function botConfigChange(key: configKey, value: number) {
let noun = "Setting toggle";
return devices
.current
.updateMcu({ [key]: value })
.then(_.noop, commandErr(noun));
}
export function settingToggle(name: configKey, bot: BotState) {
let noun = "Setting toggle";
return devices
.current
.updateMcu({
[name]: ((bot.hardware.mcu_params)[name] === 0) ? ON : OFF
})
.then(_.noop, commandErr(noun));
}
export function moveRelative(props: MoveRelProps) {
return devices
.current
.moveRelative(props)
.then(_.noop, commandErr("Relative movement"));
}
export function moveAbs(props: MoveRelProps) {
const noun = "Absolute movement";
return devices
.current
.moveAbsolute(props)
.then(_.noop, commandErr(noun));
}
export function pinToggle(pin_number: number) {
const noun = "Setting toggle";
return devices
.current
.togglePin({ pin_number })
.then(_.noop, commandErr(noun));
}
export function homeAll(speed: number) {
let noun = "'Home All' command";
devices
.current
.home({ axis: "all", speed })
.then(commandOK(noun), commandErr(noun));
}
function readStatus() {
let noun = "'Read Status' command";
return devices
.current
.readStatus()
.then(() => { commandOK(noun); }, () => { });
}
let NEED_VERSION_CHECK = true;
// Already filtering messages in FarmBot OS and the API- this is just for
// an additional layer of safety. If sensitive data ever hits a client, it will
// be reported to ROllbar for investigation.
const BAD_WORDS = ["WPA", "PSK", "PASSWORD", "NERVES"];
export function connectDevice(token: string): {} | ((dispatch: Function) => any) {
return (dispatch: Function, getState: GetState) => {
let secure = location.protocol === "https:";
let bot = new Farmbot({ token, secure });
return bot
.connect()
.then(() => {
devices.online = true;
devices.current = bot;
(window as any)["current_bot"] = bot;
readStatus()
.then(() => bot.setUserEnv({ "LAST_CLIENT_CONNECTED": JSON.stringify(new Date()) }))
.catch(() => { });
bot.on("logs", function (msg: Log) {
if (isLog(msg) && !oneOf(BAD_WORDS, msg.message.toUpperCase())) {
maybeShowLog(msg);
dispatch(init({ kind: "logs", uuid: "MUST_CHANGE", body: msg }));
} else {
throw new Error("Refusing to display log: " + JSON.stringify(msg));
}
});
bot.on("status", function (msg: BotStateTree) {
dispatch(incomingStatus(msg));
if (NEED_VERSION_CHECK) {
let IS_OK = versionOK(getState()
.bot
.hardware
.informational_settings
.controller_version, 4, 0);
if (!IS_OK) { badVersion(); }
NEED_VERSION_CHECK = false;
}
});
let alreadyToldYou = false;
bot.on("malformed", function () {
if (!alreadyToldYou) {
warning(t("FarmBot sent a malformed message. " +
"You may need to upgrade FarmBot OS. " +
"Please upgrade FarmBot OS and log back in."));
alreadyToldYou = true;
}
});
}, (err) => dispatch(fetchDeviceErr(err)));
};
}
function fetchDeviceErr(err: Error) {
return {
type: "FETCH_DEVICE_ERR",
payload: err
};
}
let startUpdate = (dispatch: Function) => {
dispatch({ type: "SETTING_UPDATE_START", payload: undefined });
};
let updateOK = (dispatch: Function, noun: string) => {
dispatch({ type: "SETTING_UPDATE_END", payload: undefined });
commandOK(noun);
};
let updateNO = (dispatch: Function, noun: string) => {
dispatch({ type: "SETTING_UPDATE_END", payload: undefined });
commandErr(noun);
};
export function updateMCU(key: configKey, val: string) {
let noun = "configuration update";
return function (dispatch: Function) {
startUpdate(dispatch);
devices
.current
.updateMcu({ [key]: val })
.then(() => updateOK(dispatch, noun))
.catch(() => updateNO(dispatch, noun));
};
}
export function updateConfig(config: Configuration) {
let noun = "Update Config";
return function (dispatch: Function) {
devices
.current
.updateConfig(config)
.then(() => updateOK(dispatch, noun))
.catch(() => updateNO(dispatch, noun));
};
}
export function changeStepSize(integer: number) {
return {
type: "CHANGE_STEP_SIZE",
payload: integer
};
}
const CHANNELS: keyof Log = "channels";
const TOAST: ALLOWED_CHANNEL_NAMES = "toast";
function maybeShowLog(log: Log) {
let chanList = _.get(log, CHANNELS, ["ERROR FETCHING CHANNELS"]);
let t = log.meta.type as ALLOWED_MESSAGE_TYPES;
const TITLE = "New message from bot";
if (chanList.includes(TOAST)) {
switch (t) {
case "success":
return success(log.message, TITLE);
case "busy":
case "warn":
case "error":
return error(log.message, TITLE);
case "fun":
case "info":
default:
return info(log.message, TITLE);
}
}
}
export function setSyncStatus(payload: SyncStatus) {
return { type: "SET_SYNC_STATUS", payload };
}
function badVersion() {
info("You are running an old version of FarmBot OS.", "Please Update", "red");
}