FarmBot/Farmbot-Web-App

View on GitHub
frontend/wizard/checks.tsx

Summary

Maintainability
F
3 days
Test Coverage
import React from "react";
import { TakePhotoButton } from "../controls/move/take_photo_button";
import { mapStateToProps } from "../controls/state_to_props";
import { store } from "../redux/store";
import { MoveControls } from "../controls/move/move_controls";
import {
  generateFarmwareDictionary, getEnv, saveOrEditFarmwareEnv,
} from "../farmware/state_to_props";
import { isBotOnlineFromState } from "../devices/must_be_online";
import {
  findUuid,
  getDeviceAccountSettings, getUserAccountSettings, selectAllAlerts,
  selectAllFarmwareEnvs,
  selectAllImages, selectAllLogs, selectAllPeripherals, selectAllSensors,
  selectAllTools,
  maybeGetTimeSettings,
} from "../resources/selectors";
import { last, some, uniq } from "lodash";
import {
  CameraCheckBaseProps,
  WizardOutcomeComponentProps,
  WizardStepComponentProps,
} from "./interfaces";
import {
  colorFromThrottle, ThrottleType,
} from "../settings/fbos_settings/fbos_details";
import { ExternalUrl } from "../external_urls";
import { getFbosConfig, getFirmwareConfig } from "../resources/getters";
import { validFbosConfig, validFwConfig } from "../util";
import { validBotLocationData } from "../util/location";
import {
  getFwHardwareValue, isExpress,
} from "../settings/firmware/firmware_hardware_support";
import { t } from "../i18next_wrapper";
import {
  BlurableInput,
  Checkbox, docLink, DropDownItem, FBSelect, genesisDocLink, Row, ToggleButton,
} from "../ui";
import {
  changeFirmwareHardware, SEED_DATA_OPTIONS, SEED_DATA_OPTIONS_DDI,
} from "../messages/cards";
import { seedAccount } from "../messages/actions";
import { FirmwareHardware, TaggedLog, Xyz } from "farmbot";
import { ConnectivityDiagram } from "../devices/connectivity/diagram";
import { Diagnosis } from "../devices/connectivity/diagnosis";
import { connectivityData } from "../devices/connectivity/generate_data";
import {
  sourceFbosConfigValue, sourceFwConfigValue,
} from "../settings/source_config_value";
import {
  emergencyUnlock, findAxisLength, findHome, setHome, settingToggle,
} from "../devices/actions";
import { NumberConfigKey } from "farmbot/dist/resources/configs/firmware";
import { calibrate } from "../photos/camera_calibration/actions";
import { cameraBtnProps } from "../photos/capture_settings/camera_selection";
import {
  CalibrationCardSVG, CameraCalibrationMethodConfig,
} from "../photos/camera_calibration";
import { envGet, prepopulateEnv } from "../photos/remote_env/selectors";
import { formatEnvKey } from "../photos/remote_env/translators";
import { Peripherals } from "../controls/peripherals";
import { ToolVerification } from "../tools/tool_verification";
import { FarmwareForm } from "../farmware/farmware_forms";
import { FarmwareName } from "../sequences/step_tiles/tile_execute_script";
import { BooleanSetting } from "../session_keys";
import {
  BooleanConfigKey as BooleanWebAppConfigKey,
} from "farmbot/dist/resources/configs/web_app";
import { GetWebAppConfigValue, toggleWebAppBool } from "../config_storage/actions";
import { PLACEHOLDER_FARMBOT } from "../photos/images/image_flipper";
import { OriginSelector } from "../settings/farm_designer_settings";
import { Sensors } from "../sensors";
import {
  DropdownConfig,
  NumberBoxConfig, NumberBoxConfigProps,
} from "../photos/camera_calibration/config";
import { Actions, DeviceSetting, SetupWizardContent, ToolTips } from "../constants";
import { WD_KEY_DEFAULTS } from "../photos/remote_env/constants";
import { LockableButton } from "../settings/hardware_settings/lockable_button";
import {
  disabledAxisMap,
} from "../settings/hardware_settings/axis_tracking_status";
import { destroy, edit, initSave, save } from "../api/crud";
import { FlashFirmwareBtn } from "../settings/firmware/firmware_hardware_status";
import {
  ORIGIN_DROPDOWNS, SPECIAL_VALUE_DDI,
} from "../photos/camera_calibration/constants";
import { tourPath } from "../help/tours";
import { TOURS } from "../help/tours/data";
import { useNavigate } from "react-router-dom";
import { FilePath } from "../internal_urls";
import { BotPositionRows } from "../controls/move/bot_position_rows";
import {
  BootSequenceSelector,
} from "../settings/fbos_settings/boot_sequence_selector";
import { getImageJobs } from "../photos/state_to_props";
import { ResourceIndex } from "../resources/interfaces";
import { BotState } from "../devices/interfaces";
import {
  reduceToolName, ToolName,
} from "../farm_designer/map/tool_graphics/all_tools";
import { WaterFlowRateInput } from "../tools/edit_tool";
import { RPI_OPTIONS } from "../settings/fbos_settings/rpi_model";
import { BoxTop } from "../settings/pin_bindings/box_top";
import { OtaTimeSelector } from "../settings/fbos_settings/ota_time_selector";

export const Language = (props: WizardStepComponentProps) => {
  const user = getUserAccountSettings(props.resources);
  return <BlurableInput
    type="text"
    name="language"
    value={user.body.language || ""}
    onCommit={e => {
      props.dispatch(edit(
        user,
        { language: e.currentTarget.value }));
      props.dispatch(save(user.uuid));
    }} />;
};

const CAMERA_ERRORS = ["Camera not detected.", "Problem getting image."];

/** Check if an item's time is after the specified time, if provided. */
export const greaterThanTime = (
  itemTime: number | undefined,
  thresholdTime: number | undefined,
) =>
  thresholdTime && (itemTime || 0) > thresholdTime;

export const recentMsgLog = (
  logs: TaggedLog[],
  prevLogTime: number | undefined,
  messages: string[],
) =>
  some(logs
    .filter(log => greaterThanTime(log.body.created_at, prevLogTime))
    .map(log => some(messages.map(message =>
      log.body.message.toLowerCase().includes(message.toLowerCase())))));

const CameraCheckBase = (props: CameraCheckBaseProps) => {
  const images = selectAllImages(props.resources);
  const getLastImageId = () => last(images)?.body.id;
  const [prevImageId, setPrevImageId] = React.useState(getLastImageId());
  const newImageUrls = images
    .filter(image => prevImageId && (image.body.id || 0) > prevImageId)
    .map(image => image.body.attachment_url);
  const imageUrl = last(newImageUrls);

  const logs = selectAllLogs(props.resources);
  const getLastLogTimestamp = () => last(logs)?.body.created_at;
  const [prevLogTime, setPrevLogTime] = React.useState(getLastLogTimestamp());
  const [error, setError] = React.useState(false);
  if (!error && recentMsgLog(logs, prevLogTime, CAMERA_ERRORS)) {
    props.setStepSuccess(false, "cameraError")();
    setError(true);
  }

  return <div className={"camera-check grid"}
    onClick={() => {
      setPrevImageId(getLastImageId());
      setPrevLogTime(getLastLogTimestamp());
      setError(false);
    }}>
    <props.component {...props} />
    <p>{props.longDuration
      ? t("Images may take up to 3 minutes to appear.")
      : t("Images may take up to 30 seconds to appear.")}</p>
    <img src={imageUrl || PLACEHOLDER_FARMBOT} />
  </div>;
};

const TakePhotoButtonComponent = (props: CameraCheckBaseProps) => {
  const env = getEnv(props.resources);
  const botOnline = isBotOnlineFromState(props.bot);
  const logs = selectAllLogs(props.resources);
  return <TakePhotoButton env={env} botOnline={botOnline}
    imageJobs={getImageJobs(props.bot.hardware.jobs)} logs={logs} />;
};

export const CameraCheck = (props: WizardStepComponentProps) =>
  <CameraCheckBase {...props} component={TakePhotoButtonComponent} />;

export const CameraCalibrationCard = (props: WizardStepComponentProps) => {
  const env = getEnv(props.resources);
  const easyCalibration = !!envGet("CAMERA_CALIBRATION_easy_calibration",
    prepopulateEnv(env));
  return <div className={"camera-calibration-card"}>
    <CalibrationCardSVG grid={easyCalibration} />
  </div>;
};

export const SwitchCameraCalibrationMethod =
  (props: WizardOutcomeComponentProps) => {
    return <CameraCalibrationMethodConfig
      wdEnvGet={key => envGet(key, prepopulateEnv(getEnv(props.resources)))}
      saveEnvVar={(key, value) =>
        props.dispatch(saveOrEditFarmwareEnv(props.resources)(
          key, JSON.stringify(formatEnvKey(key, value))))} />;
  };

const CameraCalibrationButtonComponent = (props: CameraCheckBaseProps) => {
  const env = getEnv(props.resources);
  const botOnline = isBotOnlineFromState(props.bot);
  const camDisabled = cameraBtnProps(env, botOnline);
  const easyCalibration = !!envGet("CAMERA_CALIBRATION_easy_calibration",
    prepopulateEnv(env));
  return <button
    className={`fb-button green ${camDisabled.class}`}
    disabled={!botOnline}
    title={camDisabled.title}
    onClick={camDisabled.click || calibrate(easyCalibration)}>
    {t("Calibrate")}
  </button>;
};

export const CameraCalibrationCheck = (props: WizardStepComponentProps) =>
  <CameraCheckBase {...props} component={CameraCalibrationButtonComponent} />;

const MeasureSoilHeight = (props: CameraCheckBaseProps) => {
  const farmwares = generateFarmwareDictionary(props.bot, props.resources, true);
  const env = getEnv(props.resources);
  const botOnline = isBotOnlineFromState(props.bot);
  const userEnv = props.bot.hardware.user_env;
  const farmwareEnvs = selectAllFarmwareEnvs(props.resources);
  const soilHeightFarmware = farmwares[FarmwareName.MeasureSoilHeight];
  return soilHeightFarmware
    ? <FarmwareForm
      farmware={soilHeightFarmware}
      env={env}
      userEnv={userEnv}
      farmwareEnvs={farmwareEnvs}
      saveFarmwareEnv={saveOrEditFarmwareEnv(props.resources)}
      botOnline={botOnline}
      hideAdvanced={true}
      hideResets={true}
      dispatch={props.dispatch} />
    : <button className={"fb-button gray pseudo-disabled"} disabled={true}>
      {t("Missing dependency")}
    </button>;
};

export const SoilHeightMeasurementCheck = (props: WizardStepComponentProps) =>
  <CameraCheckBase {...props} component={MeasureSoilHeight} longDuration={true} />;

export const lowVoltageProblemStatus = () => {
  const { throttled } = store.getState().bot.hardware.informational_settings;
  const voltageColor =
    colorFromThrottle(throttled || "0x0", ThrottleType.UnderVoltage);
  return !["red", "yellow"].includes(voltageColor);
};

export interface ControlsCheckOptions {
  axis?: Xyz;
  home?: boolean;
  both?: boolean;
  up?: boolean;
}

export interface ControlsCheckProps {
  dispatch: Function;
  controlsCheckOptions: ControlsCheckOptions;
}

export const ControlsCheck = (props: ControlsCheckProps) => {
  const { axis, both, home, up } = props.controlsCheckOptions;
  const upDirection = up ? "up" : undefined;
  const highlightDirection = both ? "both" : upDirection;
  return <div className={"controls-check"}>
    <MoveControls {...mapStateToProps(store.getState())}
      dispatch={props.dispatch}
      highlightAxis={axis}
      highlightDirection={highlightDirection}
      highlightHome={home} />
  </div>;
};

export const AssemblyDocs = (props: WizardOutcomeComponentProps) => {
  const firmwareHardware = getFwHardwareValue(getFbosConfig(props.resources));

  return <a href={isExpress(firmwareHardware)
    ? ExternalUrl.expressAssembly
    : ExternalUrl.genesisAssembly} target={"_blank"} rel={"noreferrer"}>
    {t("Assembly documentation")}
  </a>;
};

export const DownloadOS = (props: WizardOutcomeComponentProps) => {
  const getRelease = (rpi: string | undefined) => {
    switch (rpi) {
      case "01": return {
        imageUrl: globalConfig.rpi_release_url,
        releaseTag: globalConfig.rpi_release_tag,
      };
      case "02":
      case "3": return {
        imageUrl: globalConfig.rpi3_release_url,
        releaseTag: globalConfig.rpi3_release_tag,
      };
      case "4": return {
        imageUrl: globalConfig.rpi4_release_url,
        releaseTag: globalConfig.rpi4_release_tag,
      };
    }
  };
  const rpi = getDeviceAccountSettings(props.resources).body.rpi;
  const release = getRelease(rpi);
  return release
    ? <a className={"download-os-link"} href={release.imageUrl}>
      {`${t("Download")} FBOS v${release.releaseTag}`}
    </a>
    : <p>{t("Please select a model")}</p>;
};

export const DownloadImager = () =>
  <a className={"download-imager-link"} href={ExternalUrl.rpiImager}
    target={"_blank"} rel={"noreferrer"}>
    {t("Download Raspberry Pi Imager")}
  </a>;

export const NetworkRequirementsLink = () =>
  <a href={docLink("for-it-security-professionals")}
    target={"_blank"} rel={"noreferrer"}>
    {t("FarmBot Network Requirements")}
  </a>;

export const FlashFirmware = (props: WizardStepComponentProps) => {
  const firmwareHardware = getFwHardwareValue(getFbosConfig(props.resources));
  const botOnline = isBotOnlineFromState(props.bot);
  return <FlashFirmwareBtn
    apiFirmwareValue={firmwareHardware}
    botOnline={botOnline} />;
};

const SEED_DATA_OPTION_TO_FW_HARDWARE: Record<string, FirmwareHardware> = {
  "genesis_1.2": "arduino",
  "genesis_1.3": "farmduino",
  "genesis_1.4": "farmduino_k14",
  "genesis_1.5": "farmduino_k15",
  "genesis_1.6": "farmduino_k16",
  "genesis_1.7": "farmduino_k17",
  "genesis_xl_1.4": "farmduino_k14",
  "genesis_xl_1.5": "farmduino_k15",
  "genesis_xl_1.6": "farmduino_k16",
  "genesis_xl_1.7": "farmduino_k17",
  "express_1.0": "express_k10",
  "express_1.1": "express_k11",
  "express_1.2": "express_k12",
  "express_xl_1.0": "express_k10",
  "express_xl_1.1": "express_k11",
  "express_xl_1.2": "express_k12",
  "none": "none",
};

const FW_HARDWARE_TO_RPI: Record<FirmwareHardware, string | undefined> = {
  "arduino": "3",
  "farmduino": "3",
  "farmduino_k14": "3",
  "farmduino_k15": "3",
  "farmduino_k16": undefined,
  "farmduino_k17": "4",
  "express_k10": "01",
  "express_k11": "02",
  "express_k12": "02",
  "none": "3",
};

interface FirmwareHardwareSelectionState {
  selection: string;
  autoSeed: boolean;
  seeded: boolean;
}

export class FirmwareHardwareSelection
  extends React.Component<WizardStepComponentProps,
    FirmwareHardwareSelectionState> {
  state: FirmwareHardwareSelectionState = {
    selection: "",
    autoSeed: this.seedAlerts.length > 0,
    seeded: false,
  };

  get seedAlerts() {
    return selectAllAlerts(this.props.resources)
      .filter(alert => alert.body.problem_tag == "api.seed_data.missing");
  }

  onChange = (ddi: DropDownItem) => {
    const { dispatch, resources } = this.props;

    this.setState({ selection: "" + ddi.value });
    const firmwareHardware = SEED_DATA_OPTION_TO_FW_HARDWARE["" + ddi.value];
    changeFirmwareHardware(dispatch)({ label: "", value: firmwareHardware });
    const rpi = FW_HARDWARE_TO_RPI[firmwareHardware];
    if (rpi) {
      const device = getDeviceAccountSettings(this.props.resources);
      dispatch(edit(device, { rpi }));
      dispatch(save(device.uuid));
    }

    const seedAlertId = this.seedAlerts[0]?.body.id;
    const dismiss = () => seedAlertId && dispatch(destroy(
      findUuid(resources, "Alert", seedAlertId)));
    if (this.state.autoSeed && !this.state.seeded) {
      this.setState({ seeded: true });
      seedAccount(dismiss)({ label: "", value: ddi.value });
    }
  };

  toggleAutoSeed = () => this.setState({ autoSeed: !this.state.autoSeed });

  render() {
    const { selection, autoSeed } = this.state;
    const notSeeded = this.seedAlerts.length > 0;
    return <div className={"farmbot-model-selection"}>
      <FBSelect
        key={selection}
        list={SEED_DATA_OPTIONS()}
        selectedItem={SEED_DATA_OPTIONS_DDI()[selection]}
        onChange={this.onChange} />
      {notSeeded &&
        <div className={"seed-checkbox"}>
          <Checkbox
            onChange={this.toggleAutoSeed}
            checked={autoSeed}
            title={t("Add pre-made resources upon selection")} />
          <p>{t("Add pre-made resources upon selection")}</p>
        </div>}
      {autoSeed && notSeeded &&
        <p>{t(SetupWizardContent.SEED_DATA)}</p>}
      {autoSeed && !notSeeded &&
        <p>{t("Resources added!")}</p>}
    </div>;
  }
}

export const RpiSelection = (props: WizardStepComponentProps) => {
  const device = getDeviceAccountSettings(props.resources);
  const selection = device.body.rpi;
  return <div className={"rpi-selection"}>
    <img style={{ width: "100%" }}
      src={FilePath.setupWizardImage("rpi_3_vs_4.jpg")} />
    <FBSelect
      key={selection}
      customNullLabel={t("Select one")}
      list={Object.values(RPI_OPTIONS)
        .filter(ddi => ["3", "4"].includes("" + ddi.value))}
      selectedItem={RPI_OPTIONS["" + selection]}
      onChange={ddi => {
        const device = getDeviceAccountSettings(props.resources);
        props.dispatch(edit(device, { rpi: "" + ddi.value }));
        props.dispatch(save(device.uuid));
      }} />
  </div>;
};

export const Connectivity = (props: WizardStepComponentProps) => {
  const data = connectivityData({
    bot: props.bot,
    device: getDeviceAccountSettings(props.resources),
    apiFirmwareValue: getFwHardwareValue(getFbosConfig(props.resources)),
  });
  return <div className={"connectivity"}>
    <ConnectivityDiagram rowData={data.rowData} />
    <Diagnosis statusFlags={data.flags} />
  </div>;
};

export const AutoUpdate = (props: WizardStepComponentProps) => {
  return <OtaTimeSelector
    dispatch={props.dispatch}
    device={getDeviceAccountSettings(props.resources)}
    timeSettings={maybeGetTimeSettings(props.resources)}
    sourceFbosConfig={sourceFbosConfigValue(
      validFbosConfig(getFbosConfig(props.resources)),
      props.bot.hardware.configuration)}
  />;
};

export const InvertJogButton = (axis: Xyz) =>
  (props: WizardOutcomeComponentProps) => {
    const setting: Record<Xyz, BooleanWebAppConfigKey> = {
      x: BooleanSetting.x_axis_inverted,
      y: BooleanSetting.y_axis_inverted,
      z: BooleanSetting.z_axis_inverted,
    };

    return <fieldset>
      <label>
        {t("Invert {{ axis }}-axis Jog Buttons", { axis })}
      </label>
      <ToggleButton
        toggleAction={() => props.dispatch(toggleWebAppBool(setting[axis]))}
        toggleValue={!!props.getConfigValue(setting[axis])} />
    </fieldset>;
  };

export const CheckForResistance = () =>
  <p>
    {t("Check hardware for resistance.")}&nbsp;
    {t("Refer to the")}&nbsp;
    <a href={genesisDocLink("why-is-my-farmbot-not-moving")}>
      {t("Why is my FarmBot not moving?")}
    </a>
    &nbsp;{t("documentation page for adjustment suggestions.")}
  </p>;

export const MotorCurrentContent = () =>
  <fieldset>
    <CheckForResistance />
    <p>{t("You may also try increasing motor current by 10%.")}</p>
  </fieldset>;

const FirmwareSettingToggle = (
  setting: { key: NumberConfigKey, label: string },
  showText: boolean,
) =>
  (props: WizardOutcomeComponentProps) => {
    const sourceFwConfig = sourceFwConfigValue(validFwConfig(getFirmwareConfig(
      props.resources)), props.bot.hardware.mcu_params);
    const param = sourceFwConfig(setting.key);
    return <fieldset>
      {showText && <CheckForResistance />}
      <label>{t(setting.label)}</label>
      <ToggleButton dispatch={props.dispatch}
        dim={!param.consistent}
        toggleValue={param.value}
        toggleAction={() =>
          props.dispatch(settingToggle(setting.key, sourceFwConfig))} />
    </fieldset>;
  };

export const DisableStallDetection = (axis: Xyz, showText = true) => {
  const setting: Record<Xyz, { key: NumberConfigKey, label: string }> = {
    x: { key: "encoder_enabled_x", label: t("x-axis stall detection") },
    y: { key: "encoder_enabled_y", label: t("y-axis stall detection") },
    z: { key: "encoder_enabled_z", label: t("z-axis stall detection") },
  };
  return FirmwareSettingToggle(setting[axis], showText);
};

export const SwapJogButton = (props: WizardOutcomeComponentProps) =>
  <fieldset>
    <label>
      {t("Swap jog buttons: x and y axes")}
    </label>
    <ToggleButton
      toggleAction={() => props.dispatch(toggleWebAppBool(BooleanSetting.xy_swap))}
      toggleValue={!!props.getConfigValue(BooleanSetting.xy_swap)} />
  </fieldset>;

export const RotateMapToggle = (props: WizardOutcomeComponentProps) =>
  <fieldset>
    <label>
      {t("Rotate map")}
    </label>
    <ToggleButton
      toggleAction={() => props.dispatch(toggleWebAppBool(BooleanSetting.xy_swap))}
      toggleValue={!!props.getConfigValue(BooleanSetting.xy_swap)} />
  </fieldset>;

export const DynamicMapToggle = (props: WizardOutcomeComponentProps) =>
  <fieldset>
    <label>
      {t("Dynamic map size")}
    </label>
    <ToggleButton
      toggleAction={() =>
        props.dispatch(toggleWebAppBool(BooleanSetting.dynamic_map))}
      toggleValue={!!props.getConfigValue(BooleanSetting.dynamic_map)} />
  </fieldset>;

export const SelectMapOrigin = (props: WizardOutcomeComponentProps) =>
  <fieldset>
    <label>
      {t("Origin")}
    </label>
    <OriginSelector
      dispatch={props.dispatch}
      getConfigValue={props.getConfigValue} />
  </fieldset>;

export const MapOrientation = (props: WizardOutcomeComponentProps) =>
  <div className={"map-orientation"}>
    <RotateMapToggle {...props} />
    <SelectMapOrigin {...props} />
  </div>;

export const PeripheralsCheck = (props: WizardStepComponentProps) => {
  const peripherals = uniq(selectAllPeripherals(props.resources));
  const firmwareHardware = getFwHardwareValue(getFbosConfig(props.resources));
  return <div className={"peripherals-check"}>
    <Peripherals
      getConfigValue={props.getConfigValue}
      firmwareHardware={firmwareHardware}
      bot={props.bot}
      peripherals={peripherals}
      resources={props.resources}
      hidePinBindings={true}
      dispatch={props.dispatch} />
  </div>;
};

export interface PinBindingOptions {
  editing: boolean;
  unlockOnly?: boolean;
}

interface PinBindingProps {
  dispatch: Function;
  pinBindingOptions: PinBindingOptions;
  resources: ResourceIndex;
  bot: BotState;
  getConfigValue: GetWebAppConfigValue;
}

export const PinBinding = (props: PinBindingProps) => {
  const firmwareHardware = getFwHardwareValue(getFbosConfig(props.resources));
  return props.pinBindingOptions.unlockOnly
    ? <button
      title={t("unlock device")}
      className={"fb-button yellow e-stop"}
      onClick={() => emergencyUnlock()}>
      {t("UNLOCK")}
    </button>
    : <BoxTop
      threeDimensions={!!props.getConfigValue(
        BooleanSetting.enable_3d_electronics_box_top)}
      firmwareHardware={firmwareHardware}
      resources={props.resources}
      dispatch={props.dispatch}
      botOnline={isBotOnlineFromState(props.bot)}
      bot={props.bot}
      isEditing={props.pinBindingOptions.editing} />;
};

export const FindHome = (axis: Xyz) => (props: WizardStepComponentProps) => {
  const botOnline = isBotOnlineFromState(props.bot);
  const firmwareSettings = getFirmwareConfig(props.resources);
  const hardwareDisabled = disabledAxisMap(firmwareSettings?.body
    || props.bot.hardware.mcu_params);
  return <LockableButton
    className={"wizard-find-home-btn"}
    disabled={hardwareDisabled[axis] || !botOnline}
    title={t("FIND HOME")}
    onClick={() => findHome(axis)}>
    {t("FIND HOME {{ axis }}", { axis })}
  </LockableButton>;
};

export const SetHome = (axis: Xyz) => (props: WizardStepComponentProps) => {
  const botOnline = isBotOnlineFromState(props.bot);
  return <LockableButton
    disabled={!botOnline}
    title={t("SET HOME")}
    onClick={() => setHome(axis)}>
    {t("SET HOME {{ axis }}", { axis })}
  </LockableButton>;
};

export const AxisActions = (props: WizardStepComponentProps) => {
  const locationData = validBotLocationData(props.bot.hardware.location_data);
  const firmwareSettings = getFirmwareConfig(props.resources)?.body;
  const sourceFwConfig = sourceFwConfigValue(validFwConfig(getFirmwareConfig(
    props.resources)), props.bot.hardware.mcu_params);
  if (!firmwareSettings) { return <div />; }
  const firmwareHardware = getFwHardwareValue(getFbosConfig(props.resources));
  const botOnline = isBotOnlineFromState(props.bot);
  const { busy, locked } = props.bot.hardware.informational_settings;
  return <BotPositionRows
    locationData={locationData}
    showCurrentPosition={true}
    getConfigValue={props.getConfigValue}
    sourceFwConfig={sourceFwConfig}
    arduinoBusy={busy}
    locked={locked}
    botOnline={botOnline}
    dispatch={props.dispatch}
    firmwareSettings={firmwareSettings}
    firmwareHardware={firmwareHardware} />;
};

export const FindAxisLength = (axis: Xyz) =>
  (props: WizardOutcomeComponentProps) => {
    const botOnline = isBotOnlineFromState(props.bot);
    const firmwareSettings = getFirmwareConfig(props.resources);
    const hardwareDisabled = disabledAxisMap(firmwareSettings?.body
      || props.bot.hardware.mcu_params);
    const { busy } = props.bot.hardware.informational_settings;
    return <fieldset>
      <p>{t("Then try finding the axis length again.")}</p>
      <LockableButton
        className={"wizard-find-length-btn"}
        disabled={busy || hardwareDisabled[axis] || !botOnline}
        title={t("FIND LENGTH")}
        onClick={() => findAxisLength(axis)}>
        {t("FIND LENGTH")}
      </LockableButton>
    </fieldset>;
  };

export const BootSequence = () => {
  return <BootSequenceSelector />;
};

export const CameraOffset = (props: WizardStepComponentProps) => {
  const helpText = t(ToolTips.CAMERA_OFFSET, {
    defaultX: WD_KEY_DEFAULTS["CAMERA_CALIBRATION_camera_offset_x"],
    defaultY: WD_KEY_DEFAULTS["CAMERA_CALIBRATION_camera_offset_y"],
  });
  const env = getEnv(props.resources);
  const wDEnv = prepopulateEnv(env);
  const common: Pick<NumberBoxConfigProps, "wdEnvGet" | "onChange"> = {
    wdEnvGet: key => envGet(key, wDEnv),
    onChange: (key, value) =>
      props.dispatch(saveOrEditFarmwareEnv(props.resources)(
        key, JSON.stringify(formatEnvKey(key, value)))),
  };
  return <Row className="grid-2-col">
    <NumberBoxConfig {...common}
      settingName={DeviceSetting.cameraOffsetX}
      configKey={"CAMERA_CALIBRATION_camera_offset_x"}
      helpText={helpText} />
    <NumberBoxConfig {...common}
      settingName={DeviceSetting.cameraOffsetY}
      configKey={"CAMERA_CALIBRATION_camera_offset_y"}
      helpText={helpText} />
  </Row>;
};

export const CameraImageOrigin = (props: WizardStepComponentProps) => {
  const env = getEnv(props.resources);
  const wDEnv = prepopulateEnv(env);
  return <Row>
    <DropdownConfig
      settingName={DeviceSetting.originLocationInImage}
      wdEnvGet={key => envGet(key, wDEnv)}
      onChange={(key, value) =>
        props.dispatch(saveOrEditFarmwareEnv(props.resources)(
          key, JSON.stringify(formatEnvKey(key, value))))}
      list={ORIGIN_DROPDOWNS()}
      configKey={"CAMERA_CALIBRATION_image_bot_origin_location"}
      helpText={t(ToolTips.IMAGE_BOT_ORIGIN_LOCATION, {
        defaultOrigin: SPECIAL_VALUE_DDI()[WD_KEY_DEFAULTS[
          "CAMERA_CALIBRATION_image_bot_origin_location"]].label
      })} />
  </Row>;
};

export const FlowRateInput = (props: WizardStepComponentProps) => {
  const tool = selectAllTools(props.resources).filter(tool =>
    reduceToolName(tool.body.name) == ToolName.wateringNozzle)[0];
  return tool
    ? <WaterFlowRateInput
      value={tool.body.flow_rate_ml_per_s}
      hideTooltip={true}
      onChange={flowRate => {
        props.dispatch(edit(tool, {
          flow_rate_ml_per_s: flowRate,
        }));
        props.dispatch(save(tool.uuid));
      }} />
    : <button className={"fb-button green"}
      style={{ float: "none" }}
      title={t("Add new tool")}
      onClick={() =>
        props.dispatch(initSave("Tool", { name: "Watering Nozzle" }))}>
      {t("Add watering nozzle")}
    </button>;
};

export const ToolCheck = (props: WizardStepComponentProps) => {
  const sensors = selectAllSensors(props.resources);
  return <ToolVerification sensors={sensors} bot={props.bot} />;
};

export const SensorsCheck = (props: WizardStepComponentProps) => {
  const sensors = uniq(selectAllSensors(props.resources));
  const botOnline = isBotOnlineFromState(props.bot);
  const firmwareHardware = getFwHardwareValue(getFbosConfig(props.resources));
  return <div className={"sensors-check"}>
    <Sensors
      firmwareHardware={firmwareHardware}
      bot={props.bot}
      sensors={sensors}
      disabled={!botOnline}
      dispatch={props.dispatch} />
  </div>;
};

export const CameraReplacement = () =>
  <div className={"camera-replacement"}>
    <p></p>
    <p>
      {t(SetupWizardContent.CAMERA_REPLACEMENT)}
      <a href={ExternalUrl.Store.cameraReplacement}
        target="_blank" rel={"noreferrer"}>
        {t("here")}.
      </a>
    </p>
  </div>;

export const Tour = (tourSlug: string) => {
  const navigate = useNavigate();
  return (props: WizardStepComponentProps) =>
    <button className={"fb-button green tour-start"}
      title={t("Start tour")}
      onClick={() => {
        const firstStep = TOURS()[tourSlug].steps[0];
        props.dispatch({ type: Actions.SET_TOUR, payload: tourSlug });
        props.dispatch({ type: Actions.SET_TOUR_STEP, payload: firstStep.slug });
        navigate(tourPath(firstStep.url, tourSlug, firstStep.slug));
      }}>
      {t("Start tour")}
    </button>;
};