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

423 lines
12 KiB
TypeScript

import { t } from "i18next";
import axios from "axios";
import * as _ from "lodash";
import { success, warning, info, error } from "farmbot-toastr";
import { getDevice } from "../device";
import { Log, Everything } from "../interfaces";
import {
GithubRelease, MoveRelProps, MinOsFeatureLookup, SourceFwConfig, Axis
} from "./interfaces";
import { Thunk, ReduxAction } from "../redux/interfaces";
import { McuParams, Configuration, rpcRequest } from "farmbot";
import { Sequence } from "../sequences/interfaces";
import { ControlPanelState } from "../devices/interfaces";
import { API } from "../api/index";
import { User } from "../auth/interfaces";
import { getDeviceAccountSettings, getFirmwareConfig } from "../resources/selectors";
import { TaggedDevice, TaggedFirmwareConfig } from "../resources/tagged_resources";
import { oneOf, versionOK, trim } from "../util";
import { Actions, Content } from "../constants";
import { mcuParamValidator } from "./update_interceptor";
import { pingAPI } from "../connectivity/ping_mqtt";
import { edit, save as apiSave } from "../api/crud";
import { getFbosConfig } from "../resources/selectors_by_kind";
import { FbosConfig } from "../config_storage/fbos_configs";
import { FirmwareConfig } from "../config_storage/firmware_configs";
import { CONFIG_DEFAULTS } from "farmbot/dist/config";
const ON = 1, OFF = 0;
export type ConfigKey = keyof McuParams;
export const EXPECTED_MAJOR = 6;
export const EXPECTED_MINOR = 0;
export const FEATURE_MIN_VERSIONS_URL =
"https://raw.githubusercontent.com/FarmBot/farmbot_os/staging/" +
"FEATURE_MIN_VERSIONS.json";
// 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"];
// tslint:disable-next-line:no-any
export function isLog(x: any): x is Log {
const yup = _.isObject(x) && _.isString(_.get(x, "message" as keyof Log));
if (yup) {
if (oneOf(BAD_WORDS, x.message.toUpperCase())) {// SECURITY CRITICAL CODE.
throw new Error("Refusing to display log: " + JSON.stringify(x));
}
return true;
} else {
return false;
}
}
export const commandErr =
(noun = "Command") => () => error(t(`${noun} failed`));
export const commandOK = (noun = "Command") => () => {
const msg = t(noun) + t(" request sent to device.");
success(msg, t("Request sent"));
};
export function checkControllerUpdates() {
const noun = "Check for Updates";
commandOK(noun)();
getDevice()
.checkUpdates()
.catch(commandErr(noun));
}
export function powerOff() {
const noun = "Power Off Bot";
getDevice()
.powerOff()
.then(commandOK(noun), commandErr(noun));
}
export function factoryReset() {
if (!confirm(t(Content.FACTORY_RESET_ALERT))) {
return;
}
getDevice().resetOS();
}
export function reboot() {
const noun = "Reboot Bot";
getDevice()
.reboot()
.then(commandOK(noun), commandErr(noun));
}
export function emergencyLock() {
const noun = "Emergency stop";
getDevice()
.emergencyLock()
.then(commandOK(noun), commandErr(noun));
}
export function emergencyUnlock() {
const noun = "Emergency unlock";
if (confirm(t(`Are you sure you want to unlock the device?`))) {
getDevice()
.emergencyUnlock()
.then(commandOK(noun), commandErr(noun));
}
}
export function sync(): Thunk {
const noun = "Sync";
return function (_dispatch, getState) {
const IS_OK = versionOK(getState()
.bot
.hardware
.informational_settings
.controller_version, EXPECTED_MAJOR, EXPECTED_MINOR);
if (IS_OK) {
getDevice()
.sync()
// TODO: Probably wrong. Fix when there is time to QA - RC 5/2/18
.then(() => { commandOK(noun); })
.catch(commandErr(noun));
} else {
if (getState()
.bot
.hardware
.informational_settings
.controller_version) {
badVersion();
} else {
info(t("FarmBot is not connected."), t("Disconnected"), "red");
}
}
};
}
export function execSequence(sequence: Sequence) {
const noun = "Sequence execution";
if (sequence.id) {
commandOK(noun)();
return getDevice().execSequence(sequence.id).catch(commandErr(noun));
} else {
throw new Error("Can't execute unsaved sequences");
}
}
export function requestDiagnostic() {
const noun = "Diagnostic Request";
return getDevice().dumpInfo().then(commandOK(noun), commandErr(noun));
}
export let saveAccountChanges: Thunk = function (_dispatch, getState) {
return save(getDeviceAccountSettings(getState().resources.index));
};
export let fetchReleases =
(url: string, options = { beta: false }) =>
(dispatch: Function) => {
axios
.get<GithubRelease>(url)
.then(resp => {
const { tag_name, target_commitish } = resp.data;
const version = tag_name.toLowerCase().replace("v", "");
dispatch({
type: options.beta
? Actions.FETCH_BETA_OS_UPDATE_INFO_OK
: Actions.FETCH_OS_UPDATE_INFO_OK,
payload: { version, commit: target_commitish }
});
})
.catch((ferror) => {
!options.beta &&
error(t("Could not download FarmBot OS update information."));
dispatch({
type: options.beta
? "FETCH_BETA_OS_UPDATE_INFO_ERROR"
: "FETCH_OS_UPDATE_INFO_ERROR",
payload: ferror
});
});
};
/**
* Structure and type checks for fetched minimum FBOS version feature object.
* @param x axios response data
*/
function validMinOsFeatureLookup(x: MinOsFeatureLookup): boolean {
return _.isObject(x) &&
Object.entries(x).every(([key, val]) =>
typeof key === "string" && // feature name
typeof val === "string" && // version string
val.split(".").length > 2); // "0.0.0"
}
/**
* Fetch and save minimum FBOS version data for UI feature display.
* @param url location of data
*/
export let fetchMinOsFeatureData = (url: string) =>
(dispatch: Function) => {
axios
.get<MinOsFeatureLookup>(url)
.then(resp => {
const data = resp.data;
if (validMinOsFeatureLookup(data)) {
dispatch({
type: Actions.FETCH_MIN_OS_FEATURE_INFO_OK,
payload: data
});
} else {
console.log(`Warning! Got '${JSON.stringify(data)}', ` +
"expected min OS feature data.");
}
})
.catch((ferror) => {
dispatch({
type: "FETCH_MIN_OS_FEATURE_INFO_ERROR",
payload: ferror
});
});
};
export function save(input: TaggedDevice) {
return function (dispatch: Function) {
return axios
.put<User>(API.current.devicePath, input.body)
.then(resp => dispatch({ type: "SAVE_DEVICE_OK", payload: resp.data }))
.catch(() => error(t("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 bulkToggleControlPanel(payload: boolean) {
return { type: "BULK_TOGGLE_CONTROL_PANEL", payload };
}
export function MCUFactoryReset() {
if (!confirm(t(Content.MCU_RESET_ALERT))) {
return;
}
return getDevice().resetMCU().catch(commandErr("MCU Reset"));
}
export function settingToggle(
name: ConfigKey,
sourceFwConfig: SourceFwConfig,
displayAlert?: string | undefined
) {
const noun = "Setting toggle";
return function (dispatch: Function, getState: () => Everything) {
if (displayAlert) { alert(trim(displayAlert)); }
const update = { [name]: (sourceFwConfig(name).value === 0) ? ON : OFF };
const firmwareConfig = getFirmwareConfig(getState().resources.index);
const toggleFirmwareConfig = (fwConfig: TaggedFirmwareConfig) => {
dispatch(edit(fwConfig, update));
dispatch(apiSave(fwConfig.uuid));
};
if (firmwareConfig && firmwareConfig.body.api_migrated) {
return toggleFirmwareConfig(firmwareConfig);
} else {
return getDevice()
.updateMcu(update)
.then(_.noop, commandErr(noun));
}
};
}
export function moveRelative(props: MoveRelProps) {
return getDevice()
.moveRelative(props)
.then(_.noop, commandErr("Relative movement"));
}
export function moveAbs(props: MoveRelProps) {
const noun = "Absolute movement";
return getDevice()
.moveAbsolute(props)
.then(_.noop, commandErr(noun));
}
export function pinToggle(pin_number: number) {
const noun = "Setting toggle";
return getDevice()
.togglePin({ pin_number })
.then(_.noop, commandErr(noun));
}
export function readPin(pin_number: number, label: string, pin_mode: number) {
const noun = "Read pin";
return getDevice()
.send(rpcRequest([{
kind: "read_pin", args: { pin_number, label, pin_mode }
}]))
.then(_.noop, commandErr(noun));
}
export function homeAll(speed: number) {
const noun = "'Home All' command";
getDevice()
.home({ axis: "all", speed })
.catch(commandErr(noun));
}
export function findHome(axis: Axis, speed = CONFIG_DEFAULTS.speed) {
const noun = "'Find Home' command";
getDevice()
.findHome({ axis, speed })
.catch(commandErr(noun));
}
const startUpdate = () => {
return {
type: Actions.SETTING_UPDATE_START,
payload: undefined
};
};
const updateOK = (dispatch: Function, noun: string) => {
dispatch({ type: "SETTING_UPDATE_END", payload: undefined });
commandOK(noun);
};
const updateNO = (dispatch: Function, noun: string) => {
dispatch({ type: "SETTING_UPDATE_END", payload: undefined });
commandErr(noun);
};
export function updateMCU(key: ConfigKey, val: string) {
const noun = "configuration update";
return function (dispatch: Function, getState: () => Everything) {
const firmwareConfig = getFirmwareConfig(getState().resources.index);
const getParams = () => {
if (firmwareConfig && firmwareConfig.body.api_migrated) {
return firmwareConfig.body;
} else {
return getState().bot.hardware.mcu_params;
}
};
function proceed() {
if (firmwareConfig && firmwareConfig.body.api_migrated) {
dispatch(edit(firmwareConfig, { [key]: val } as Partial<FirmwareConfig>));
dispatch(apiSave(firmwareConfig.uuid));
} else {
dispatch(startUpdate());
getDevice()
.updateMcu({ [key]: val })
.then(() => updateOK(dispatch, noun))
.catch(() => updateNO(dispatch, noun));
}
}
const dont = (err: string) => warning(err);
const validate = mcuParamValidator(key, parseInt(val, 10), getParams());
validate(proceed, dont);
};
}
export function updateConfig(config: Configuration) {
const noun = "Update Config";
return function (dispatch: Function, getState: () => Everything) {
const fbosConfig = getFbosConfig(getState().resources.index);
if (fbosConfig && fbosConfig.body.api_migrated) {
dispatch(edit(fbosConfig, config as Partial<FbosConfig>));
dispatch(apiSave(fbosConfig.uuid));
} else {
getDevice()
.updateConfig(config)
.then(_.noop, commandErr(noun));
}
};
}
export function registerGpioPin(
pinBinding: { pin_number: number, sequence_id: number }) {
const noun = "Register GPIO Pin";
return function (dispatch: Function) {
getDevice()
.registerGpio(pinBinding)
.then(() => updateOK(dispatch, noun))
.catch(() => updateNO(dispatch, noun));
};
}
export function unregisterGpioPin(pin_number: number) {
const noun = "Unregister GPIO Pin";
return function (dispatch: Function) {
getDevice()
.unregisterGpio({ pin_number })
.then(() => updateOK(dispatch, noun))
.catch(() => updateNO(dispatch, noun));
};
}
export function changeStepSize(integer: number) {
return {
type: Actions.CHANGE_STEP_SIZE,
payload: integer
};
}
export function badVersion() {
info(t("You are running an old version of FarmBot OS."), t("Please Update"), "red");
}
/** Change all device statuses to "unknown" */
export function resetNetwork(): ReduxAction<{}> {
return { type: Actions.RESET_NETWORK, payload: {} };
}
export function resetConnectionInfo() {
return function (dispatch: Function) {
dispatch(resetNetwork());
pingAPI();
getDevice().readStatus();
};
}