add none camera type

pull/1642/head
gabrielburnworth 2019-12-27 10:37:54 -08:00
parent 4fa48cb74b
commit 22465a5558
68 changed files with 655 additions and 436 deletions

View File

@ -0,0 +1,17 @@
import { HardwareFlags, FarmwareData } from "../sequences/interfaces";
export const fakeHardwareFlags = (): HardwareFlags => ({
findHomeEnabled: { x: false, y: false, z: false },
stopAtHome: { x: false, y: false, z: false },
stopAtMax: { x: false, y: false, z: false },
negativeOnly: { x: false, y: false, z: false },
axisLength: { x: 0, y: 0, z: 0 },
});
export const fakeFarmwareData = (): FarmwareData => ({
farmwareNames: [],
firstPartyFarmwareNames: [],
showFirstPartyFarmware: false,
farmwareConfigs: {},
cameraDisabled: false,
});

View File

@ -1,11 +0,0 @@
import { HardwareFlags } from "../sequences/interfaces";
export const fakeHardwareFlags = (): HardwareFlags => {
return {
findHomeEnabled: { x: false, y: false, z: false },
stopAtHome: { x: false, y: false, z: false },
stopAtMax: { x: false, y: false, z: false },
negativeOnly: { x: false, y: false, z: false },
axisLength: { x: 0, y: 0, z: 0 },
};
};

View File

@ -9,7 +9,7 @@ import { RawApp as App, AppProps, mapStateToProps } from "../app";
import { mount } from "enzyme";
import { bot } from "../__test_support__/fake_state/bot";
import {
fakeUser, fakeWebAppConfig
fakeUser, fakeWebAppConfig, fakeFbosConfig, fakeFarmwareEnv
} from "../__test_support__/fake_state/resources";
import { fakeState } from "../__test_support__/fake_state";
import {
@ -40,7 +40,8 @@ const fakeProps = (): AppProps => ({
resources: buildResourceIndex().index,
autoSync: false,
alertCount: 0,
pings: fakePings()
pings: fakePings(),
env: {},
});
describe("<App />: Controls Pop-Up", () => {
@ -145,7 +146,24 @@ describe("mapStateToProps()", () => {
const config = fakeWebAppConfig();
config.body.x_axis_inverted = true;
state.resources = buildResourceIndex([config]);
state.bot.hardware.user_env = { fake: "value" };
const result = mapStateToProps(state);
expect(result.axisInversion.x).toEqual(true);
expect(result.autoSync).toEqual(false);
expect(result.env).toEqual({ fake: "value" });
});
it("returns api props", () => {
const state = fakeState();
const config = fakeFbosConfig();
config.body.auto_sync = true;
config.body.api_migrated = true;
const fakeEnv = fakeFarmwareEnv();
state.resources = buildResourceIndex([config, fakeEnv]);
state.bot.minOsFeatureData = { api_farmware_env: "8.0.0" };
state.bot.hardware.informational_settings.controller_version = "8.0.0";
const result = mapStateToProps(state);
expect(result.autoSync).toEqual(true);
expect(result.env).toEqual({ [fakeEnv.body.key]: fakeEnv.body.value });
});
});

View File

@ -9,38 +9,37 @@ import { ControlsPopup } from "../controls_popup";
import { mount } from "enzyme";
import { bot } from "../__test_support__/fake_state/bot";
import { ControlsPopupProps } from "../controls/move/interfaces";
import { error } from "../toast/toast";
import { Content, ToolTips } from "../constants";
describe("<ControlsPopup />", () => {
const fakeProps = (): ControlsPopupProps => {
return {
dispatch: jest.fn(),
axisInversion: { x: false, y: false, z: false },
botPosition: { x: undefined, y: undefined, z: undefined },
firmwareSettings: bot.hardware.mcu_params,
xySwap: false,
arduinoBusy: false,
stepSize: 100,
botOnline: true,
};
};
const p = fakeProps();
p.axisInversion.x = true;
const wrapper = mount(<ControlsPopup {...p} />);
afterAll(wrapper.unmount);
const fakeProps = (): ControlsPopupProps => ({
dispatch: jest.fn(),
axisInversion: { x: true, y: false, z: false },
botPosition: { x: undefined, y: undefined, z: undefined },
firmwareSettings: bot.hardware.mcu_params,
xySwap: false,
arduinoBusy: false,
stepSize: 100,
botOnline: true,
env: {},
});
it("Has a false initial state", () => {
const wrapper = mount(<ControlsPopup {...fakeProps()} />);
expect(wrapper.state("isOpen")).toBeFalsy();
});
it("Toggles state", () => {
const wrapper = mount(<ControlsPopup {...fakeProps()} />);
const parent = wrapper.find("i").first();
parent.simulate("click");
expect(wrapper.state("isOpen")).toBeTruthy();
});
it("x axis is inverted", () => {
const wrapper = mount(<ControlsPopup {...fakeProps()} />);
wrapper.setState({ isOpen: true });
const button = wrapper.find("button").at(3);
expect(button.props().title).toBe("move x axis (100)");
button.simulate("click");
@ -49,6 +48,8 @@ describe("<ControlsPopup />", () => {
});
it("y axis is not inverted", () => {
const wrapper = mount(<ControlsPopup {...fakeProps()} />);
wrapper.setState({ isOpen: true });
const button = wrapper.find("button").at(1);
expect(button.props().title).toBe("move y axis (100)");
button.simulate("click");
@ -57,6 +58,7 @@ describe("<ControlsPopup />", () => {
});
it("disabled when closed", () => {
const wrapper = mount(<ControlsPopup {...fakeProps()} />);
wrapper.setState({ isOpen: false });
[0, 1, 2, 3].map((i) => wrapper.find("button").at(i).simulate("click"));
expect(mockDevice.moveRelative).not.toHaveBeenCalled();
@ -65,6 +67,7 @@ describe("<ControlsPopup />", () => {
it("swaps axes", () => {
const swappedProps = fakeProps();
swappedProps.xySwap = true;
swappedProps.axisInversion.x = false;
const swapped = mount(<ControlsPopup {...swappedProps} />);
swapped.setState({ isOpen: true });
expect(swapped.state("isOpen")).toBeTruthy();
@ -76,7 +79,25 @@ describe("<ControlsPopup />", () => {
});
it("takes photo", () => {
wrapper.find("button").at(4).simulate("click");
const wrapper = mount(<ControlsPopup {...fakeProps()} />);
wrapper.setState({ isOpen: true });
const btn = wrapper.find("button").at(4);
expect(btn.props().title).not.toEqual(Content.NO_CAMERA_SELECTED);
btn.simulate("click");
expect(mockDevice.takePhoto).toHaveBeenCalled();
expect(error).not.toHaveBeenCalled();
});
it("shows camera as disabled", () => {
const p = fakeProps();
p.env = { camera: "NONE" };
const wrapper = mount(<ControlsPopup {...p} />);
wrapper.setState({ isOpen: true });
const btn = wrapper.find("button").at(4);
expect(btn.props().title).toEqual(Content.NO_CAMERA_SELECTED);
btn.simulate("click");
expect(error).toHaveBeenCalledWith(
ToolTips.SELECT_A_CAMERA, Content.NO_CAMERA_SELECTED);
expect(mockDevice.takePhoto).not.toHaveBeenCalled();
});
});

View File

@ -4,7 +4,7 @@ import { init, error } from "./toast/toast";
import { NavBar } from "./nav";
import { Everything, TimeSettings } from "./interfaces";
import { LoadingPlant } from "./loading_plant";
import { BotState, Xyz } from "./devices/interfaces";
import { BotState, Xyz, UserEnv } from "./devices/interfaces";
import { ResourceName, TaggedUser, TaggedLog } from "farmbot";
import {
maybeFetchUser,
@ -30,6 +30,7 @@ import { isBotOnline } from "./devices/must_be_online";
import { getStatus } from "./connectivity/reducer_support";
import { getAllAlerts } from "./messages/state_to_props";
import { PingDictionary } from "./devices/connectivity/qos";
import { getEnv, getShouldDisplayFn } from "./farmware/state_to_props";
/** For the logger module */
init();
@ -52,11 +53,14 @@ export interface AppProps {
autoSync: boolean;
alertCount: number;
pings: PingDictionary;
env: UserEnv;
}
export function mapStateToProps(props: Everything): AppProps {
const webAppConfigValue = getWebAppConfigValue(() => props);
const fbosConfig = validFbosConfig(getFbosConfig(props.resources.index));
const shouldDisplay = getShouldDisplayFn(props.resources.index, props.bot);
const env = getEnv(props.resources.index, shouldDisplay, props.bot);
return {
timeSettings: maybeGetTimeSettings(props.resources.index),
dispatch: props.dispatch,
@ -78,7 +82,8 @@ export function mapStateToProps(props: Everything): AppProps {
resources: props.resources.index,
autoSync: !!(fbosConfig && fbosConfig.auto_sync),
alertCount: getAllAlerts(props.resources).length,
pings: props.bot.connectivity.pings
pings: props.bot.connectivity.pings,
env,
};
}
/** Time at which the app gives up and asks the user to refresh */
@ -147,6 +152,7 @@ export class RawApp extends React.Component<AppProps, {}> {
xySwap={this.props.xySwap}
arduinoBusy={!!this.props.bot.hardware.informational_settings.busy}
botOnline={isBotOnline(sync_status, getStatus(bot2mqtt))}
env={this.props.env}
stepSize={this.props.bot.stepSize} />}
</div>;
}

View File

@ -285,6 +285,9 @@ export namespace ToolTips {
trim(`Snaps a photo using the device camera. Select the camera type
on the Device page.`);
export const SELECT_A_CAMERA =
trim(`Select a camera on the Device page to take photos.`);
export const MARK_AS =
trim(`The Mark As step allows FarmBot to programmatically edit the
properties of the UTM, plants, and weeds from within a sequence.
@ -822,6 +825,9 @@ export namespace Content {
export const NOT_AVAILABLE_WHEN_OFFLINE =
trim(`Not available when device is offline.`);
export const NO_CAMERA_SELECTED =
trim(`No camera selected`);
}
export namespace TourContent {

View File

@ -24,6 +24,7 @@ describe("<Controls />", () => {
getWebAppConfigVal: jest.fn((key) => (mockConfig[key])),
sensorReadings: [],
timeSettings: fakeTimeSettings(),
env: {},
});
it("shows webcam widget", () => {

View File

@ -1,6 +1,6 @@
import { mapStateToProps } from "../state_to_props";
import { buildResourceIndex } from "../../__test_support__/resource_index_builder";
import { fakeUser } from "../../__test_support__/fake_state/resources";
import { fakeUser, fakeFarmwareEnv } from "../../__test_support__/fake_state/resources";
import { fakeState } from "../../__test_support__/fake_state";
describe("mapStateToProps()", () => {
@ -10,4 +10,14 @@ describe("mapStateToProps()", () => {
const result = mapStateToProps(state);
expect(result.timeSettings).toEqual({ utcOffset: 0, hour24: false });
});
it("returns api props", () => {
const state = fakeState();
const fakeEnv = fakeFarmwareEnv();
state.resources = buildResourceIndex([fakeEnv]);
state.bot.minOsFeatureData = { api_farmware_env: "8.0.0" };
state.bot.hardware.informational_settings.controller_version = "8.0.0";
const result = mapStateToProps(state);
expect(result.env).toEqual({ [fakeEnv.body.key]: fakeEnv.body.value });
});
});

View File

@ -29,6 +29,7 @@ export class RawControls extends React.Component<Props, {}> {
move = () => <Move
bot={this.props.bot}
env={this.props.env}
dispatch={this.props.dispatch}
arduinoBusy={this.arduinoBusy}
botToMqttStatus={this.props.botToMqttStatus}

View File

@ -1,4 +1,6 @@
import { BotState, Xyz, BotPosition, ShouldDisplay } from "../devices/interfaces";
import {
BotState, Xyz, BotPosition, ShouldDisplay, UserEnv
} from "../devices/interfaces";
import { Vector3, McuParams } from "farmbot/dist";
import {
TaggedWebcamFeed,
@ -22,6 +24,7 @@ export interface Props {
getWebAppConfigVal: GetWebAppConfigValue;
sensorReadings: TaggedSensorReading[];
timeSettings: TimeSettings;
env: UserEnv;
}
export interface AxisDisplayGroupProps {

View File

@ -15,6 +15,8 @@ import { mount } from "enzyme";
import { JogButtons } from "../jog_buttons";
import { JogMovementControlsProps } from "../interfaces";
import { bot } from "../../../__test_support__/fake_state/bot";
import { error } from "../../../toast/toast";
import { Content, ToolTips } from "../../../constants";
describe("<JogButtons/>", function () {
const jogButtonProps = (): JogMovementControlsProps => {
@ -26,6 +28,7 @@ describe("<JogButtons/>", function () {
firmwareSettings: bot.hardware.mcu_params,
xySwap: false,
doFindHome: false,
env: {},
};
};
@ -60,8 +63,12 @@ describe("<JogButtons/>", function () {
it("takes photo", () => {
const jogButtons = mount(<JogButtons {...jogButtonProps()} />);
jogButtons.find("button").at(0).simulate("click");
const cameraBtn = jogButtons.find("button").at(0);
expect(cameraBtn.props().title).not.toEqual(Content.NO_CAMERA_SELECTED);
cameraBtn.simulate("click");
expect(mockDevice.takePhoto).toHaveBeenCalled();
expect(error).not.toHaveBeenCalled();
});
it("error taking photo", () => {
@ -71,6 +78,18 @@ describe("<JogButtons/>", function () {
expect(mockDevice.takePhoto).toHaveBeenCalled();
});
it("shows camera as disabled", () => {
const p = jogButtonProps();
p.env = { camera: "NONE" };
const jogButtons = mount(<JogButtons {...p} />);
const cameraBtn = jogButtons.find("button").at(0);
expect(cameraBtn.props().title).toEqual(Content.NO_CAMERA_SELECTED);
cameraBtn.simulate("click");
expect(error).toHaveBeenCalledWith(
ToolTips.SELECT_A_CAMERA, Content.NO_CAMERA_SELECTED);
expect(mockDevice.takePhoto).not.toHaveBeenCalled();
});
it("has unswapped xy jog buttons", () => {
const jogButtons = mount(<JogButtons {...jogButtonProps()} />);
const button = jogButtons.find("button").at(6);

View File

@ -1,16 +1,15 @@
const mockDevice = {
moveAbsolute: jest.fn(() => { return Promise.resolve(); }),
};
const mockDevice = { moveAbsolute: jest.fn(() => Promise.resolve()) };
jest.mock("../../../device", () => ({ getDevice: () => mockDevice }));
jest.mock("../../../device", () => ({
getDevice: () => (mockDevice)
jest.mock("../../../config_storage/actions", () => ({
toggleWebAppBool: jest.fn(),
}));
jest.mock("../../../config_storage/actions", () => {
return {
toggleWebAppBool: jest.fn()
};
});
jest.mock("../../../account/dev/dev_support", () => ({
DevSettings: {
futureFeaturesEnabled: () => false,
}
}));
import * as React from "react";
import { mount, shallow } from "enzyme";
@ -26,16 +25,15 @@ import { clickButton } from "../../../__test_support__/helpers";
describe("<Move />", () => {
const mockConfig: Dictionary<boolean> = {};
function fakeProps(): MoveProps {
return {
dispatch: jest.fn(),
bot: bot,
arduinoBusy: false,
botToMqttStatus: "up",
firmwareSettings: bot.hardware.mcu_params,
getWebAppConfigVal: jest.fn((key) => (mockConfig[key])),
};
}
const fakeProps = (): MoveProps => ({
dispatch: jest.fn(),
bot: bot,
arduinoBusy: false,
botToMqttStatus: "up",
firmwareSettings: bot.hardware.mcu_params,
getWebAppConfigVal: jest.fn((key) => (mockConfig[key])),
env: {},
});
it("has default elements", () => {
const wrapper = mount(<Move {...fakeProps()} />);

View File

@ -1,4 +1,4 @@
import { BotPosition, BotState } from "../../devices/interfaces";
import { BotPosition, BotState, UserEnv } from "../../devices/interfaces";
import { McuParams, Xyz } from "farmbot";
import { NetworkState } from "../../connectivity/interfaces";
import { GetWebAppConfigValue } from "../../config_storage/actions";
@ -14,6 +14,7 @@ export interface MoveProps {
botToMqttStatus: NetworkState;
firmwareSettings: McuParams;
getWebAppConfigVal: GetWebAppConfigValue;
env: UserEnv;
}
export interface DirectionButtonProps {
@ -47,6 +48,7 @@ interface JogMovementControlsBaseProps extends DirectionAxesProps {
stepSize: number;
arduinoBusy: boolean;
xySwap: boolean;
env: UserEnv;
}
export interface JogMovementControlsProps extends JogMovementControlsBaseProps {

View File

@ -5,6 +5,9 @@ import { JogMovementControlsProps } from "./interfaces";
import { getDevice } from "../../device";
import { buildDirectionProps } from "./direction_axes_props";
import { t } from "../../i18next_wrapper";
import {
cameraBtnProps
} from "../../devices/components/fbos_settings/camera_selection";
const DEFAULT_STEP_SIZE = 100;
/*
@ -20,35 +23,37 @@ export function JogButtons(props: JogMovementControlsProps) {
const directionAxesProps = buildDirectionProps(props);
const rightLeft = xySwap ? "y" : "x";
const upDown = xySwap ? "x" : "y";
const commonProps = {
steps: stepSize || DEFAULT_STEP_SIZE,
disabled: arduinoBusy
};
const camDisabled = cameraBtnProps(props.env);
return <table className="jog-table">
<tbody>
<tr>
<td>
<button
className="i fa fa-camera arrow-button fb-button"
title={t("Take a photo")}
onClick={() => getDevice().takePhoto().catch(() => { })} />
className={`fa fa-camera arrow-button fb-button ${
camDisabled.class}`}
title={camDisabled.title || t("Take a photo")}
onClick={camDisabled.click ||
(() => getDevice().takePhoto().catch(() => { }))} />
</td>
<td />
<td />
<td>
<DirectionButton
<DirectionButton {...commonProps}
axis={upDown}
direction="up"
directionAxisProps={directionAxesProps[upDown]}
steps={stepSize || DEFAULT_STEP_SIZE}
disabled={arduinoBusy} />
directionAxisProps={directionAxesProps[upDown]} />
</td>
<td />
<td />
<td>
<DirectionButton
<DirectionButton {...commonProps}
axis="z"
direction="up"
directionAxisProps={directionAxesProps.z}
steps={stepSize || DEFAULT_STEP_SIZE}
disabled={arduinoBusy} />
directionAxisProps={directionAxesProps.z} />
</td>
</tr>
<tr>
@ -61,37 +66,29 @@ export function JogButtons(props: JogMovementControlsProps) {
</td>
<td />
<td>
<DirectionButton
<DirectionButton {...commonProps}
axis={rightLeft}
direction="left"
directionAxisProps={directionAxesProps[rightLeft]}
steps={stepSize || DEFAULT_STEP_SIZE}
disabled={arduinoBusy} />
directionAxisProps={directionAxesProps[rightLeft]} />
</td>
<td>
<DirectionButton
<DirectionButton {...commonProps}
axis={upDown}
direction="down"
directionAxisProps={directionAxesProps[upDown]}
steps={stepSize || DEFAULT_STEP_SIZE}
disabled={arduinoBusy} />
directionAxisProps={directionAxesProps[upDown]} />
</td>
<td>
<DirectionButton
<DirectionButton {...commonProps}
axis={rightLeft}
direction="right"
directionAxisProps={directionAxesProps[rightLeft]}
steps={stepSize || DEFAULT_STEP_SIZE}
disabled={arduinoBusy} />
directionAxisProps={directionAxesProps[rightLeft]} />
</td>
<td />
<td>
<DirectionButton
<DirectionButton {...commonProps}
axis="z"
direction="down"
directionAxisProps={directionAxesProps.z}
steps={stepSize || DEFAULT_STEP_SIZE}
disabled={arduinoBusy} />
directionAxisProps={directionAxesProps.z} />
</td>
</tr>
<tr>

View File

@ -1,6 +1,6 @@
import * as React from "react";
import { McuParams } from "farmbot";
import { BotPosition } from "../../devices/interfaces";
import { BotPosition, UserEnv } from "../../devices/interfaces";
import { changeStepSize } from "../../devices/actions";
import { StepSizeSelector } from "./step_size_selector";
import { GetWebAppBool } from "./interfaces";
@ -15,6 +15,7 @@ interface JogControlsGroupProps {
getValue: GetWebAppBool;
arduinoBusy: boolean;
firmwareSettings: McuParams;
env: UserEnv;
}
export const JogControlsGroup = (props: JogControlsGroupProps) => {
@ -38,6 +39,7 @@ export const JogControlsGroup = (props: JogControlsGroupProps) => {
z: getValue(BooleanSetting.z_axis_inverted)
}}
arduinoBusy={arduinoBusy}
env={props.env}
firmwareSettings={firmwareSettings}
xySwap={getValue(BooleanSetting.xy_swap)}
doFindHome={getValue(BooleanSetting.home_button_homing)} />

View File

@ -47,6 +47,7 @@ export class Move extends React.Component<MoveProps, {}> {
botPosition={locationData.position}
getValue={this.getValue}
arduinoBusy={this.props.arduinoBusy}
env={this.props.env}
firmwareSettings={this.props.firmwareSettings} />
<BotPositionRows
locationData={locationData}

View File

@ -3,31 +3,23 @@ import {
selectAllPeripherals,
selectAllWebcamFeeds,
selectAllSensors,
maybeGetDevice,
selectAllSensorReadings,
maybeGetTimeSettings
} from "../resources/selectors";
import { Props } from "./interfaces";
import {
validFwConfig,
createShouldDisplayFn as shouldDisplayFunc,
determineInstalledOsVersion
} from "../util";
import { validFwConfig } from "../util";
import { getWebAppConfigValue } from "../config_storage/actions";
import { getFirmwareConfig } from "../resources/getters";
import { uniq } from "lodash";
import { getStatus } from "../connectivity/reducer_support";
import { DevSettings } from "../account/dev/dev_support";
import { getEnv, getShouldDisplayFn } from "../farmware/state_to_props";
export function mapStateToProps(props: Everything): Props {
const fwConfig = validFwConfig(getFirmwareConfig(props.resources.index));
const { mcu_params } = props.bot.hardware;
const device = maybeGetDevice(props.resources.index);
const installedOsVersion = determineInstalledOsVersion(props.bot, device);
const fbosVersionOverride = DevSettings.overriddenFbosVersion();
const shouldDisplay = shouldDisplayFunc(
installedOsVersion, props.bot.minOsFeatureData, fbosVersionOverride);
const shouldDisplay = getShouldDisplayFn(props.resources.index, props.bot);
const env = getEnv(props.resources.index, shouldDisplay, props.bot);
return {
feeds: selectAllWebcamFeeds(props.resources.index),
@ -41,5 +33,6 @@ export function mapStateToProps(props: Everything): Props {
shouldDisplay,
sensorReadings: selectAllSensorReadings(props.resources.index),
timeSettings: maybeGetTimeSettings(props.resources.index),
env,
};
}

View File

@ -5,6 +5,10 @@ import { buildDirectionProps } from "./controls/move/direction_axes_props";
import { ControlsPopupProps } from "./controls/move/interfaces";
import { commandErr } from "./devices/actions";
import { mapPanelClassName } from "./farm_designer/map/util";
import {
cameraBtnProps
} from "./devices/components/fbos_settings/camera_selection";
import { t } from "./i18next_wrapper";
interface State {
isOpen: boolean;
@ -23,40 +27,38 @@ export class ControlsPopup
const directionAxesProps = buildDirectionProps(this.props);
const rightLeft = xySwap ? "y" : "x";
const upDown = xySwap ? "x" : "y";
const movementDisabled = !isOpen || arduinoBusy || !botOnline;
const commonProps = { steps: stepSize, disabled: movementDisabled };
const camDisabled = cameraBtnProps(this.props.env);
return <div
className={`controls-popup ${isOpen} ${mapPanelClassName()}`}>
<i className="fa fa-crosshairs"
onClick={this.toggle("isOpen")} />
<div className="controls-popup-menu-outer">
<div className="controls-popup-menu-inner">
<DirectionButton
<DirectionButton {...commonProps}
axis={rightLeft}
direction="right"
directionAxisProps={directionAxesProps[rightLeft]}
steps={stepSize}
disabled={!isOpen || arduinoBusy || !botOnline} />
<DirectionButton
directionAxisProps={directionAxesProps[rightLeft]} />
<DirectionButton {...commonProps}
axis={upDown}
direction="up"
directionAxisProps={directionAxesProps[upDown]}
steps={stepSize}
disabled={!isOpen || arduinoBusy || !botOnline} />
<DirectionButton
directionAxisProps={directionAxesProps[upDown]} />
<DirectionButton {...commonProps}
axis={upDown}
direction="down"
directionAxisProps={directionAxesProps[upDown]}
steps={stepSize}
disabled={!isOpen || arduinoBusy || !botOnline} />
<DirectionButton
directionAxisProps={directionAxesProps[upDown]} />
<DirectionButton {...commonProps}
axis={rightLeft}
direction="left"
directionAxisProps={directionAxesProps[rightLeft]}
steps={stepSize}
disabled={!isOpen || arduinoBusy || !botOnline} />
directionAxisProps={directionAxesProps[rightLeft]} />
<button
className="i fa fa-camera arrow-button fb-button brown"
disabled={!botOnline}
onClick={() => getDevice().takePhoto().catch(commandErr("Photo"))} />
className={`fa fa-camera arrow-button fb-button brown ${
camDisabled.class}`}
disabled={!isOpen || !botOnline}
title={camDisabled.title || t("Take a photo")}
onClick={camDisabled.click ||
(() => getDevice().takePhoto().catch(commandErr("Photo")))} />
</div>
</div>
</div>;

View File

@ -279,6 +279,7 @@
&.pseudo-disabled {
background: $medium_light_gray !important;
box-shadow: 0 2px 0px 0px lighten($medium_light_gray, 5%) !important;
border-bottom: none !important;
&:focus,
&:hover,
&.active {

View File

@ -1,26 +1,20 @@
const mockDevice = {
setUserEnv: jest.fn(() => { return Promise.resolve(); }),
};
jest.mock("../../../../device", () => ({
getDevice: () => mockDevice
}));
const mockDevice = { setUserEnv: jest.fn(() => Promise.resolve()) };
jest.mock("../../../../device", () => ({ getDevice: () => mockDevice }));
import * as React from "react";
import { mount, shallow } from "enzyme";
import { CameraSelection } from "../camera_selection";
import { CameraSelection, cameraDisabled } from "../camera_selection";
import { CameraSelectionProps } from "../interfaces";
import { info, error } from "../../../../toast/toast";
describe("<CameraSelection/>", () => {
const fakeProps = (): CameraSelectionProps => {
return {
env: {},
botOnline: true,
shouldDisplay: () => false,
saveFarmwareEnv: jest.fn(),
dispatch: jest.fn(),
};
};
const fakeProps = (): CameraSelectionProps => ({
env: {},
botOnline: true,
shouldDisplay: () => false,
saveFarmwareEnv: jest.fn(),
dispatch: jest.fn(),
});
it("doesn't render camera", () => {
const cameraSelection = mount(<CameraSelection {...fakeProps()} />);
@ -66,3 +60,15 @@ describe("<CameraSelection/>", () => {
expect(p.saveFarmwareEnv).toHaveBeenCalledWith("camera", "\"mycamera\"");
});
});
describe("cameraDisabled()", () => {
it("returns enabled", () => {
expect(cameraDisabled({ camera: "USB" })).toEqual(false);
expect(cameraDisabled({ camera: "" })).toEqual(false);
});
it("returns disabled", () => {
expect(cameraDisabled({ camera: "none" })).toEqual(true);
expect(cameraDisabled({ camera: "\"NONE\"" })).toEqual(true);
});
});

View File

@ -6,25 +6,56 @@ import {
import { info, success, error } from "../../../toast/toast";
import { getDevice } from "../../../device";
import { ColWidth } from "../farmbot_os_settings";
import { Feature } from "../../interfaces";
import { Feature, UserEnv } from "../../interfaces";
import { t } from "../../../i18next_wrapper";
import { Content, ToolTips } from "../../../constants";
/** Check if the camera has been disabled. */
export const cameraDisabled = (env: UserEnv): boolean =>
parseCameraSelection(env) === Camera.NONE;
/** `disabled` and `title` props for buttons with actions that use the camera. */
export const cameraBtnProps = (env: UserEnv) => {
const disabled = cameraDisabled(env);
return disabled
? {
class: "pseudo-disabled",
click: () =>
error(t(ToolTips.SELECT_A_CAMERA), t(Content.NO_CAMERA_SELECTED)),
title: t(Content.NO_CAMERA_SELECTED)
}
: { class: "", click: undefined, title: "" };
};
enum Camera {
USB = "USB",
RPI = "RPI",
NONE = "NONE",
}
const parseCameraSelection = (env: UserEnv): Camera => {
const camera = env["camera"]?.toUpperCase();
if (camera?.includes(Camera.NONE)) {
return Camera.NONE;
} else if (camera?.includes(Camera.RPI)) {
return Camera.RPI;
} else {
return Camera.USB;
}
};
const CAMERA_CHOICES = () => ([
{ label: t("USB Camera"), value: "USB" },
{ label: t("Raspberry Pi Camera"), value: "RPI" }
{ label: t("USB Camera"), value: Camera.USB },
{ label: t("Raspberry Pi Camera"), value: Camera.RPI },
{ label: t("None"), value: Camera.NONE },
]);
const CAMERA_CHOICES_DDI = () => {
const CHOICES = CAMERA_CHOICES();
return {
[CHOICES[0].value]: {
label: CHOICES[0].label,
value: CHOICES[0].value
},
[CHOICES[1].value]: {
label: CHOICES[1].label,
value: CHOICES[1].value
}
[CHOICES[0].value]: { label: CHOICES[0].label, value: CHOICES[0].value },
[CHOICES[1].value]: { label: CHOICES[1].label, value: CHOICES[1].value },
[CHOICES[2].value]: { label: CHOICES[2].label, value: CHOICES[2].value },
};
};
@ -35,12 +66,8 @@ export class CameraSelection
cameraStatus: ""
};
selectedCamera(): DropDownItem {
const camera = this.props.env["camera"];
return camera
? CAMERA_CHOICES_DDI()[JSON.parse(camera)]
: CAMERA_CHOICES_DDI()["USB"];
}
selectedCamera = (): DropDownItem =>
CAMERA_CHOICES_DDI()[parseCameraSelection(this.props.env)]
sendOffConfig = (selectedCamera: DropDownItem) => {
const { props } = this;

View File

@ -209,8 +209,8 @@ export interface SensorsProps {
export interface FarmwareProps {
dispatch: Function;
env: Partial<WD_ENV>;
user_env: UserEnv;
wDEnv: Partial<WD_ENV>;
env: UserEnv;
images: TaggedImage[];
currentImage: TaggedImage | undefined;
botToMqttStatus: NetworkState;

View File

@ -1,38 +1,28 @@
import { Everything } from "../interfaces";
import { Props, Feature } from "./interfaces";
import { Props } from "./interfaces";
import {
selectAllImages,
getDeviceAccountSettings,
maybeGetDevice,
maybeGetTimeSettings,
} from "../resources/selectors";
import {
sourceFbosConfigValue, sourceFwConfigValue
} from "./components/source_config_value";
import { validFwConfig, validFbosConfig } from "../util";
import {
determineInstalledOsVersion, validFwConfig, validFbosConfig,
createShouldDisplayFn as shouldDisplayFunc
} from "../util";
import {
saveOrEditFarmwareEnv, reduceFarmwareEnv
saveOrEditFarmwareEnv, getEnv, getShouldDisplayFn
} from "../farmware/state_to_props";
import { getFbosConfig, getFirmwareConfig, getWebAppConfig } from "../resources/getters";
import { DevSettings } from "../account/dev/dev_support";
import {
getFbosConfig, getFirmwareConfig, getWebAppConfig
} from "../resources/getters";
import { getAllAlerts } from "../messages/state_to_props";
export function mapStateToProps(props: Everything): Props {
const { hardware } = props.bot;
const fbosConfig = validFbosConfig(getFbosConfig(props.resources.index));
const firmwareConfig = validFwConfig(getFirmwareConfig(props.resources.index));
const installedOsVersion = determineInstalledOsVersion(
props.bot, maybeGetDevice(props.resources.index));
const fbosVersionOverride = DevSettings.overriddenFbosVersion();
const shouldDisplay = shouldDisplayFunc(installedOsVersion,
props.bot.minOsFeatureData,
fbosVersionOverride);
const env = shouldDisplay(Feature.api_farmware_env)
? reduceFarmwareEnv(props.resources.index)
: props.bot.hardware.user_env;
const shouldDisplay = getShouldDisplayFn(props.resources.index, props.bot);
const env = getEnv(props.resources.index, shouldDisplay, props.bot);
const webAppConfig = getWebAppConfig(props.resources.index);
if (!webAppConfig) {
throw new Error("Missing web app config");

View File

@ -13,7 +13,6 @@ import {
findSequenceById,
findRegimenById,
getDeviceAccountSettings,
maybeGetDevice,
maybeGetTimeSettings
} from "../../resources/selectors";
import {
@ -22,11 +21,7 @@ import {
TaggedRegimen
} from "farmbot";
import { DropDownItem } from "../../ui/index";
import {
validFbosConfig,
createShouldDisplayFn as shouldDisplayFunc,
determineInstalledOsVersion
} from "../../util";
import { validFbosConfig } from "../../util";
import {
sourceFbosConfigValue
} from "../../devices/components/source_config_value";
@ -34,7 +29,7 @@ import { hasId } from "../../resources/util";
import { ExecutableType } from "farmbot/dist/resources/api_resources";
import { getFbosConfig } from "../../resources/getters";
import { t } from "../../i18next_wrapper";
import { DevSettings } from "../../account/dev/dev_support";
import { getShouldDisplayFn } from "../../farmware/state_to_props";
export const formatTime = (input: string, timeSettings: TimeSettings) => {
const iso = new Date(input).toISOString();
@ -143,12 +138,6 @@ export function mapStateToPropsAddEdit(props: Everything): AddEditFarmEventProps
const autoSyncEnabled =
!!sourceFbosConfigValue(fbosConfig, configuration)("auto_sync").value;
const installedOsVersion = determineInstalledOsVersion(
props.bot, maybeGetDevice(props.resources.index));
const fbosVersionOverride = DevSettings.overriddenFbosVersion();
const shouldDisplay = shouldDisplayFunc(
installedOsVersion, props.bot.minOsFeatureData, fbosVersionOverride);
return {
deviceTimezone: dev
.body
@ -167,6 +156,6 @@ export function mapStateToPropsAddEdit(props: Everything): AddEditFarmEventProps
timeSettings: maybeGetTimeSettings(props.resources.index),
autoSyncEnabled,
resources: props.resources.index,
shouldDisplay,
shouldDisplay: getShouldDisplayFn(props.resources.index, props.bot),
};
}

View File

@ -9,24 +9,17 @@ import {
selectAllPlantTemplates,
selectAllSensorReadings,
selectAllSensors,
maybeGetDevice,
maybeGetTimeSettings
} from "../resources/selectors";
import {
validBotLocationData, validFwConfig, unpackUUID,
createShouldDisplayFn as shouldDisplayFunc,
determineInstalledOsVersion
} from "../util";
import { validBotLocationData, validFwConfig, unpackUUID } from "../util";
import { getWebAppConfigValue } from "../config_storage/actions";
import { Props } from "./interfaces";
import { TaggedPlant } from "./map/interfaces";
import { RestResources } from "../resources/interfaces";
import { isString, uniq, chain } from "lodash";
import { BooleanSetting } from "../session_keys";
import { Feature } from "../devices/interfaces";
import { reduceFarmwareEnv } from "../farmware/state_to_props";
import { getEnv, getShouldDisplayFn } from "../farmware/state_to_props";
import { getFirmwareConfig } from "../resources/getters";
import { DevSettings } from "../account/dev/dev_support";
import { calcMicrostepsPerMm } from "../controls/move/direction_axes_props";
const plantFinder = (plants: TaggedPlant[]) =>
@ -84,14 +77,8 @@ export function mapStateToProps(props: Everything): Props {
.reverse()
.value();
const installedOsVersion = determineInstalledOsVersion(
props.bot, maybeGetDevice(props.resources.index));
const fbosVersionOverride = DevSettings.overriddenFbosVersion();
const shouldDisplay = shouldDisplayFunc(
installedOsVersion, props.bot.minOsFeatureData, fbosVersionOverride);
const env = shouldDisplay(Feature.api_farmware_env)
? reduceFarmwareEnv(props.resources.index)
: props.bot.hardware.user_env;
const shouldDisplay = getShouldDisplayFn(props.resources.index, props.bot);
const env = getEnv(props.resources.index, shouldDisplay, props.bot);
const cameraCalibrationData = {
scale: env["CAMERA_CALIBRATION_coord_scale"],

View File

@ -93,7 +93,7 @@ describe("<FarmwareForm />", () => {
const fakeProps = (): FarmwareFormProps => {
return {
farmware: fakeFarmware(),
user_env: {},
env: {},
dispatch: jest.fn(),
shouldDisplay: () => false,
saveFarmwareEnv: jest.fn(),

View File

@ -16,8 +16,8 @@ describe("<FarmwarePage />", () => {
const fakeProps = (): FarmwareProps => ({
farmwares: fakeFarmwares(),
botToMqttStatus: "up",
wDEnv: {},
env: {},
user_env: {},
dispatch: jest.fn(),
currentImage: undefined,
images: [],

View File

@ -1,14 +1,15 @@
let mockLastUrlChunk = "farmware";
jest.mock("../../util/urls", () => ({
urlFriendly: jest.fn(x => x),
lastUrlChunk: jest.fn(() => mockLastUrlChunk)
}));
jest.mock("../../util/urls", () => {
return {
urlFriendly: jest.fn(x => x),
lastUrlChunk: jest.fn(() => mockLastUrlChunk)
};
});
jest.mock("../../redux/store", () => ({ store: { dispatch: jest.fn() } }));
jest.mock("../../redux/store", () => ({
store: { dispatch: jest.fn() }
jest.mock("../../account/dev/dev_support", () => ({
DevSettings: {
futureFeaturesEnabled: () => false,
}
}));
import { setActiveFarmwareByName } from "../set_active_farmware_by_name";

View File

@ -43,7 +43,7 @@ describe("mapStateToProps()", () => {
const state = fakeState();
state.bot.hardware.user_env = env;
const props = mapStateToProps(state);
expect(props.user_env).toEqual(env);
expect(props.env).toEqual(env);
});
it("returns API farmware env", () => {
@ -53,7 +53,7 @@ describe("mapStateToProps()", () => {
DevSettings.MAX_FBOS_VERSION_OVERRIDE;
state.resources = buildResourceIndex([fakeFarmwareEnv()]);
const props = mapStateToProps(state);
expect(props.user_env).toEqual({
expect(props.env).toEqual({
fake_FarmwareEnv_key: "fake_FarmwareEnv_value"
});
});

View File

@ -1,6 +1,5 @@
const mockDevice = { setUserEnv: jest.fn(() => Promise.resolve({})) };
jest.mock("../../../device", () => ({ getDevice: () => mockDevice }));
jest.mock("../actions", () => ({ scanImage: jest.fn() }));
jest.mock("../../images/actions", () => ({ selectImage: jest.fn() }));
@ -11,12 +10,15 @@ import { CameraCalibrationProps } from "../interfaces";
import { scanImage } from "../actions";
import { selectImage } from "../../images/actions";
import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings";
import { error } from "../../../toast/toast";
import { Content, ToolTips } from "../../../constants";
describe("<CameraCalibration/>", () => {
const fakeProps = (): CameraCalibrationProps => ({
dispatch: jest.fn(),
currentImage: undefined,
images: [],
wDEnv: {},
env: {},
iteration: 1,
morph: 2,
@ -95,4 +97,23 @@ describe("<CameraCalibration/>", () => {
expect(p.saveFarmwareEnv).toHaveBeenCalledWith(
"CAMERA_CALIBRATION_image_bot_origin_location", "\"BOTTOM_LEFT\"");
});
it("shows calibrate as enabled", () => {
const wrapper = shallow(<CameraCalibration {...fakeProps()} />);
const btn = wrapper.find("button").first();
expect(btn.text()).toEqual("Calibrate");
expect(btn.props().title).not.toEqual(Content.NO_CAMERA_SELECTED);
expect(error).not.toHaveBeenCalled();
});
it("shows calibrate as disabled when camera is disabled", () => {
const p = fakeProps();
p.env = { camera: "NONE" };
const wrapper = shallow(<CameraCalibration {...p} />);
const btn = wrapper.find("button").first();
expect(btn.props().title).toEqual(Content.NO_CAMERA_SELECTED);
btn.simulate("click");
expect(error).toHaveBeenCalledWith(
ToolTips.SELECT_A_CAMERA, Content.NO_CAMERA_SELECTED);
});
});

View File

@ -13,6 +13,9 @@ import { Feature } from "../../devices/interfaces";
import { namespace } from "../weed_detector";
import { t } from "../../i18next_wrapper";
import { formatEnvKey } from "../weed_detector/remote_env/translators";
import {
cameraBtnProps
} from "../../devices/components/fbos_settings/camera_selection";
export class CameraCalibration extends
React.Component<CameraCalibrationProps, {}> {
@ -29,6 +32,7 @@ export class CameraCalibration extends
: envSave(key, value)
render() {
const camDisabled = cameraBtnProps(this.props.env);
return <div className="weed-detector">
<div className="farmware-button">
<MustBeOnline
@ -37,8 +41,9 @@ export class CameraCalibration extends
hideBanner={true}
lockOpen={process.env.NODE_ENV !== "production"}>
<button
onClick={this.props.dispatch(calibrate)}
className="fb-button green">
className={`fb-button green ${camDisabled.class}`}
title={camDisabled.title}
onClick={camDisabled.click || this.props.dispatch(calibrate)}>
{t("Calibrate")}
</button>
</MustBeOnline>
@ -68,9 +73,9 @@ export class CameraCalibration extends
S_HI={this.props.S_HI}
V_HI={this.props.V_HI}
invertHue={!!envGet(this.namespace("invert_hue_selection"),
this.props.env)} />
this.props.wDEnv)} />
<WeedDetectorConfig
values={this.props.env}
values={this.props.wDEnv}
onChange={this.saveEnvVar} />
</MustBeOnline>
</Col>

View File

@ -1,14 +1,17 @@
import { TaggedImage, SyncStatus } from "farmbot";
import { WD_ENV } from "../weed_detector/remote_env/interfaces";
import { NetworkState } from "../../connectivity/interfaces";
import { ShouldDisplay, SaveFarmwareEnv } from "../../devices/interfaces";
import {
ShouldDisplay, SaveFarmwareEnv, UserEnv
} from "../../devices/interfaces";
import { TimeSettings } from "../../interfaces";
export interface CameraCalibrationProps {
dispatch: Function;
images: TaggedImage[];
currentImage: TaggedImage | undefined;
env: Partial<WD_ENV>;
wDEnv: Partial<WD_ENV>;
env: UserEnv;
iteration: number;
morph: number;
blur: number;

View File

@ -11,7 +11,7 @@ import { t } from "../i18next_wrapper";
export interface FarmwareFormProps {
farmware: FarmwareManifestInfo;
user_env: UserEnv;
env: UserEnv;
shouldDisplay: ShouldDisplay;
saveFarmwareEnv: SaveFarmwareEnv;
dispatch: Function;
@ -73,7 +73,7 @@ export function FarmwareForm(props: FarmwareFormProps): JSX.Element {
/** Get a Farmware input value from FBOS. */
function getValue(farmwareName: string, currentConfig: FarmwareConfig) {
return (user_env[getConfigEnvName(farmwareName, currentConfig.name)]
return (env[getConfigEnvName(farmwareName, currentConfig.name)]
|| toString(currentConfig.value));
}
@ -87,7 +87,7 @@ export function FarmwareForm(props: FarmwareFormProps): JSX.Element {
getDevice().execScript(farmwareName, pairs).catch(() => { });
}
const { farmware, user_env } = props;
const { farmware, env } = props;
return <Col key={farmware.name}>
<div className={kebabCase(farmware.name)}>
<button

View File

@ -26,8 +26,8 @@ describe("<ImageFlipper/>", () => {
it("defaults to index 0 and flips up", () => {
const p = fakeProps();
const x = shallow(<ImageFlipper {...p} />);
const up = (x.instance() as ImageFlipper).go(1);
const flipper = shallow<ImageFlipper>(<ImageFlipper {...p} />);
const up = flipper.instance().go(1);
up();
expect(p.onFlip).toHaveBeenCalledWith(p.images[1].uuid);
});
@ -35,8 +35,8 @@ describe("<ImageFlipper/>", () => {
it("flips down", () => {
const p = fakeProps();
p.currentImage = p.images[1];
const x = shallow(<ImageFlipper {...p} />);
const down = (x.instance() as ImageFlipper).go(-1);
const flipper = shallow<ImageFlipper>(<ImageFlipper {...p} />);
const down = flipper.instance().go(-1);
down();
expect(p.onFlip).toHaveBeenCalledWith(p.images[0].uuid);
});
@ -44,8 +44,8 @@ describe("<ImageFlipper/>", () => {
it("stops at upper end", () => {
const p = fakeProps();
p.currentImage = p.images[2];
const x = shallow(<ImageFlipper {...p} />);
const up = (x.instance() as ImageFlipper).go(1);
const flipper = shallow<ImageFlipper>(<ImageFlipper {...p} />);
const up = flipper.instance().go(1);
up();
expect(p.onFlip).not.toHaveBeenCalled();
});
@ -53,8 +53,8 @@ describe("<ImageFlipper/>", () => {
it("stops at lower end", () => {
const p = fakeProps();
p.currentImage = p.images[0];
const x = shallow(<ImageFlipper {...p} />);
const down = (x.instance() as ImageFlipper).go(-1);
const flipper = shallow<ImageFlipper>(<ImageFlipper {...p} />);
const down = flipper.instance().go(-1);
down();
expect(p.onFlip).not.toHaveBeenCalled();
});
@ -118,4 +118,12 @@ describe("<ImageFlipper/>", () => {
const wrapper = mount(<ImageFlipper {...p} />);
expect(wrapper.find("img").last().props().src).toEqual(PLACEHOLDER_FARMBOT);
});
it("knows when image is loaded", () => {
const wrapper = mount<ImageFlipper>(<ImageFlipper {...fakeProps()} />);
const image = shallow(wrapper.instance().imageJSX());
expect(wrapper.state().isLoaded).toEqual(false);
image.find("img").simulate("load");
expect(wrapper.state().isLoaded).toEqual(true);
});
});

View File

@ -16,6 +16,7 @@ import { PhotosProps } from "../interfaces";
import { selectImage } from "../actions";
import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings";
import { success, error } from "../../../toast/toast";
import { Content, ToolTips } from "../../../constants";
describe("<Photos/>", () => {
const fakeProps = (): PhotosProps => ({
@ -26,6 +27,7 @@ describe("<Photos/>", () => {
imageJobs: [],
botToMqttStatus: "up",
syncStatus: "synced",
env: {},
});
it("shows photo", () => {
@ -44,9 +46,25 @@ describe("<Photos/>", () => {
it("takes photo", async () => {
const wrapper = mount(<Photos {...fakeProps()} />);
const btn = wrapper.find("button").first();
expect(btn.props().title).not.toEqual(Content.NO_CAMERA_SELECTED);
await clickButton(wrapper, 0, "take photo");
expect(mockDevice.takePhoto).toHaveBeenCalled();
await expect(success).toHaveBeenCalled();
expect(error).not.toHaveBeenCalled();
});
it("shows disabled take photo button", () => {
const p = fakeProps();
p.env = { camera: "NONE" };
const wrapper = mount(<Photos {...p} />);
const btn = wrapper.find("button").first();
expect(btn.text()).toEqual("Take Photo");
expect(btn.props().title).toEqual(Content.NO_CAMERA_SELECTED);
btn.simulate("click");
expect(error).toHaveBeenCalledWith(
ToolTips.SELECT_A_CAMERA, Content.NO_CAMERA_SELECTED);
expect(mockDevice.takePhoto).not.toHaveBeenCalled();
});
it("fails to take photo", async () => {

View File

@ -1,6 +1,7 @@
import { TaggedImage, JobProgress, SyncStatus } from "farmbot";
import { NetworkState } from "../../connectivity/interfaces";
import { TimeSettings } from "../../interfaces";
import { UserEnv } from "../../devices/interfaces";
export interface ImageFlipperProps {
onFlip(uuid: string | undefined): void;
@ -22,6 +23,7 @@ export interface PhotosProps {
imageJobs: JobProgress[];
botToMqttStatus: NetworkState;
syncStatus: SyncStatus | undefined;
env: UserEnv;
}
export interface PhotoButtonsProps {
@ -30,4 +32,5 @@ export interface PhotoButtonsProps {
imageJobs: JobProgress[],
botToMqttStatus: NetworkState;
syncStatus: SyncStatus | undefined;
env: UserEnv;
}

View File

@ -16,6 +16,9 @@ import { startCase } from "lodash";
import { MustBeOnline } from "../../devices/must_be_online";
import { t } from "../../i18next_wrapper";
import { TimeSettings } from "../../interfaces";
import {
cameraBtnProps
} from "../../devices/components/fbos_settings/camera_selection";
interface MetaInfoProps {
/** Default conversion is `attr_name ==> Attr Name`.
@ -52,6 +55,7 @@ const PhotoMetaData = ({ image }: { image: TaggedImage | undefined }) =>
const PhotoButtons = (props: PhotoButtonsProps) => {
const imageUploadJobProgress = downloadProgress(props.imageJobs[0]);
const camDisabled = cameraBtnProps(props.env);
return <div className="farmware-button">
<MustBeOnline
syncStatus={props.syncStatus}
@ -59,8 +63,9 @@ const PhotoButtons = (props: PhotoButtonsProps) => {
hideBanner={true}
lockOpen={process.env.NODE_ENV !== "production"}>
<button
className="fb-button green"
onClick={props.takePhoto}>
className={`fb-button green ${camDisabled.class}`}
title={camDisabled.title}
onClick={camDisabled.click || props.takePhoto}>
{t("Take Photo")}
</button>
</MustBeOnline>
@ -125,6 +130,7 @@ export class Photos extends React.Component<PhotosProps, {}> {
botToMqttStatus={this.props.botToMqttStatus}
takePhoto={this.takePhoto}
deletePhoto={this.deletePhoto}
env={this.props.env}
imageJobs={this.props.imageJobs} />
<ImageFlipper
onFlip={id => this.props.dispatch(selectImage(id))}

View File

@ -24,6 +24,7 @@ import { t } from "../i18next_wrapper";
import { isBotOnline } from "../devices/must_be_online";
import { BooleanSetting } from "../session_keys";
import { Dictionary } from "farmbot";
import { WDENVKey } from "./weed_detector/remote_env/interfaces";
/** Get the correct help text for the provided Farmware. */
const getToolTipByFarmware =
@ -136,6 +137,7 @@ export class RawFarmwarePage extends React.Component<FarmwareProps, {}> {
/** Load Farmware input panel contents for 1st & 3rd party Farmware. */
getPanelByFarmware(farmwareName: string) {
const wDEnvGet = (key: WDENVKey) => envGet(key, this.props.wDEnv);
switch (farmwareUrlFriendly(farmwareName)) {
case "take_photo":
case "photos":
@ -145,6 +147,7 @@ export class RawFarmwarePage extends React.Component<FarmwareProps, {}> {
timeSettings={this.props.timeSettings}
dispatch={this.props.dispatch}
images={this.props.images}
env={this.props.env}
currentImage={this.props.currentImage}
imageJobs={this.props.imageJobs} />;
case "camera_calibration":
@ -153,17 +156,18 @@ export class RawFarmwarePage extends React.Component<FarmwareProps, {}> {
dispatch={this.props.dispatch}
currentImage={this.props.currentImage}
images={this.props.images}
wDEnv={this.props.wDEnv}
env={this.props.env}
saveFarmwareEnv={this.props.saveFarmwareEnv}
iteration={envGet("CAMERA_CALIBRATION_iteration", this.props.env)}
morph={envGet("CAMERA_CALIBRATION_morph", this.props.env)}
blur={envGet("CAMERA_CALIBRATION_blur", this.props.env)}
H_LO={envGet("CAMERA_CALIBRATION_H_LO", this.props.env)}
S_LO={envGet("CAMERA_CALIBRATION_S_LO", this.props.env)}
V_LO={envGet("CAMERA_CALIBRATION_V_LO", this.props.env)}
H_HI={envGet("CAMERA_CALIBRATION_H_HI", this.props.env)}
S_HI={envGet("CAMERA_CALIBRATION_S_HI", this.props.env)}
V_HI={envGet("CAMERA_CALIBRATION_V_HI", this.props.env)}
iteration={wDEnvGet("CAMERA_CALIBRATION_iteration")}
morph={wDEnvGet("CAMERA_CALIBRATION_morph")}
blur={wDEnvGet("CAMERA_CALIBRATION_blur")}
H_LO={wDEnvGet("CAMERA_CALIBRATION_H_LO")}
S_LO={wDEnvGet("CAMERA_CALIBRATION_S_LO")}
V_LO={wDEnvGet("CAMERA_CALIBRATION_V_LO")}
H_HI={wDEnvGet("CAMERA_CALIBRATION_H_HI")}
S_HI={wDEnvGet("CAMERA_CALIBRATION_S_HI")}
V_HI={wDEnvGet("CAMERA_CALIBRATION_V_HI")}
timeSettings={this.props.timeSettings}
shouldDisplay={this.props.shouldDisplay}
botToMqttStatus={this.props.botToMqttStatus} />;
@ -174,7 +178,7 @@ export class RawFarmwarePage extends React.Component<FarmwareProps, {}> {
const farmware = getFarmwareByName(this.props.farmwares, farmwareName);
return farmware && needsFarmwareForm(farmware)
? <FarmwareForm farmware={farmware}
user_env={this.props.user_env}
env={this.props.env}
shouldDisplay={this.props.shouldDisplay}
saveFarmwareEnv={this.props.saveFarmwareEnv}
botOnline={this.botOnline}

View File

@ -3,7 +3,7 @@ import {
selectAllImages, maybeGetDevice, maybeGetTimeSettings
} from "../resources/selectors";
import {
FarmwareProps, Feature, SaveFarmwareEnv, UserEnv
FarmwareProps, Feature, SaveFarmwareEnv, UserEnv, ShouldDisplay, BotState
} from "../devices/interfaces";
import { prepopulateEnv } from "./weed_detector/remote_env/selectors";
import {
@ -11,7 +11,7 @@ import {
} from "../resources/selectors_by_kind";
import {
determineInstalledOsVersion,
createShouldDisplayFn as shouldDisplayFunc,
createShouldDisplayFn,
betterCompact
} from "../util";
import { ResourceIndex } from "../resources/interfaces";
@ -51,6 +51,20 @@ export const reduceFarmwareEnv =
return farmwareEnv;
};
export const getEnv =
(ri: ResourceIndex, shouldDisplay: ShouldDisplay, bot: BotState) =>
shouldDisplay(Feature.api_farmware_env)
? reduceFarmwareEnv(ri)
: bot.hardware.user_env;
export const getShouldDisplayFn = (ri: ResourceIndex, bot: BotState) => {
const lookupData = bot.minOsFeatureData;
const installed = determineInstalledOsVersion(bot, maybeGetDevice(ri));
const override = DevSettings.overriddenFbosVersion();
const shouldDisplay = createShouldDisplayFn(installed, lookupData, override);
return shouldDisplay;
};
export function mapStateToProps(props: Everything): FarmwareProps {
const images = chain(selectAllImages(props.resources.index))
.sortBy(x => x.body.id)
@ -64,14 +78,8 @@ export function mapStateToProps(props: Everything): FarmwareProps {
const { currentFarmware, firstPartyFarmwareNames, infoOpen } =
props.resources.consumers.farmware;
const installedOsVersion = determineInstalledOsVersion(
props.bot, maybeGetDevice(props.resources.index));
const fbosVersionOverride = DevSettings.overriddenFbosVersion();
const shouldDisplay = shouldDisplayFunc(
installedOsVersion, props.bot.minOsFeatureData, fbosVersionOverride);
const env = shouldDisplay(Feature.api_farmware_env)
? reduceFarmwareEnv(props.resources.index)
: props.bot.hardware.user_env;
const shouldDisplay = getShouldDisplayFn(props.resources.index, props.bot);
const env = getEnv(props.resources.index, shouldDisplay, props.bot);
const taggedFarmwareInstallations =
selectAllFarmwareInstallations(props.resources.index);
@ -115,8 +123,8 @@ export function mapStateToProps(props: Everything): FarmwareProps {
currentFarmware,
farmwares,
botToMqttStatus,
env: prepopulateEnv(env),
user_env: env,
wDEnv: prepopulateEnv(env),
env,
dispatch: props.dispatch,
currentImage,
images,

View File

@ -23,7 +23,7 @@ jest.mock("../../../util", () => ({
}));
import { deletePoints } from "../actions";
import { scanImage, test } from "../actions";
import { scanImage, detectPlants } from "../actions";
import axios from "axios";
import { API } from "../../../api";
import { times } from "lodash";
@ -44,10 +44,10 @@ describe("scanImage()", () => {
});
});
describe("test()", () => {
describe("detectPlants()", () => {
it("calls out to the device", () => {
// Run function to invoke side effects
const thunk = test();
const thunk = detectPlants();
thunk();
// Ensure the side effects were the ones we expected.
expect(mockDevice.execScript)

View File

@ -1,11 +1,16 @@
import * as React from "react";
import { mount, shallow } from "enzyme";
import { WeedDetectorConfig } from "../config";
import { SettingsMenuProps } from "../interfaces";
describe("<WeedDetectorConfig />", () => {
const fakeProps = (): SettingsMenuProps => ({
values: {},
onChange: jest.fn(),
});
it("renders", () => {
const wrapper = mount(<WeedDetectorConfig
values={{}} onChange={jest.fn()} />);
const wrapper = mount(<WeedDetectorConfig {...fakeProps()} />);
["Invert Hue Range Selection",
"Calibration Object Separation",
"Calibration Object Separation along axis",
@ -15,15 +20,39 @@ describe("<WeedDetectorConfig />", () => {
.map(string => expect(wrapper.text()).toContain(string));
});
it("changes value", () => {
const onChange = jest.fn();
const wrapper = shallow(<WeedDetectorConfig
values={{}} onChange={onChange} />);
it("changes axis value", () => {
const p = fakeProps();
const wrapper = shallow(<WeedDetectorConfig {...p} />);
const input = wrapper.find("FBSelect").first();
input.simulate("change", { label: "", value: 4 });
expect(onChange).toHaveBeenCalledWith(
expect(p.onChange).toHaveBeenCalledWith(
"CAMERA_CALIBRATION_calibration_along_axis", 4);
const badChange = () => input.simulate("change", { label: "", value: "4" });
expect(badChange).toThrow("Weed detector got a non-numeric value");
});
it("changes hue invert value", () => {
const p = fakeProps();
const wrapper = shallow(<WeedDetectorConfig {...p} />);
const input = wrapper.find("input").first();
input.simulate("change", { currentTarget: { checked: true } });
expect(p.onChange).toHaveBeenCalledWith(
"CAMERA_CALIBRATION_invert_hue_selection", 1);
input.simulate("change", { currentTarget: { checked: false } });
expect(p.onChange).toHaveBeenCalledWith(
"CAMERA_CALIBRATION_invert_hue_selection", 0);
});
it("changes number value", () => {
const p = fakeProps();
const wrapper = shallow<WeedDetectorConfig>(<WeedDetectorConfig {...p} />);
const numBox = wrapper.instance().NumberBox({
conf: "CAMERA_CALIBRATION_blur", label: "label"
});
const NumBox = shallow(numBox);
NumBox.find("BlurableInput").first().simulate("commit", {
currentTarget: { value: "1.23" }
});
expect(p.onChange).toHaveBeenCalledWith("CAMERA_CALIBRATION_blur", 1.23);
});
});

View File

@ -1,11 +1,15 @@
const mockDevice = {
execScript: jest.fn(() => Promise.resolve()),
setUserEnv: jest.fn(() => Promise.resolve()),
};
const mockDevice = { setUserEnv: jest.fn(() => Promise.resolve()) };
jest.mock("../../../device", () => ({ getDevice: () => mockDevice }));
jest.mock("../../images/actions", () => ({ selectImage: jest.fn() }));
const mockDeletePoints = jest.fn();
jest.mock("../actions", () => ({
deletePoints: mockDeletePoints,
scanImage: jest.fn(),
detectPlants: jest.fn(),
}));
import * as React from "react";
import { mount, shallow } from "enzyme";
import { WeedDetector, namespace } from "../index";
@ -14,6 +18,9 @@ import { API } from "../../../api";
import { selectImage } from "../../images/actions";
import { clickButton } from "../../../__test_support__/helpers";
import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings";
import { deletePoints, detectPlants, scanImage } from "../actions";
import { error } from "../../../toast/toast";
import { Content, ToolTips } from "../../../constants";
describe("<WeedDetector />", () => {
API.setBaseUrl("http://localhost:3000");
@ -22,8 +29,8 @@ describe("<WeedDetector />", () => {
timeSettings: fakeTimeSettings(),
farmwares: {},
botToMqttStatus: "up",
wDEnv: {},
env: {},
user_env: {},
dispatch: jest.fn(),
currentImage: undefined,
images: [],
@ -54,16 +61,38 @@ describe("<WeedDetector />", () => {
const p = fakeProps();
p.dispatch = jest.fn(x => x());
const wrapper = shallow(<WeedDetector {...p} />);
const btn = wrapper.find("button").first();
expect(btn.props().title).not.toEqual(Content.NO_CAMERA_SELECTED);
clickButton(wrapper, 0, "detect weeds");
expect(mockDevice.execScript).toHaveBeenCalledWith("plant-detection");
expect(detectPlants).toHaveBeenCalled();
expect(error).not.toHaveBeenCalled();
});
it("shows detection button as disabled when camera is disabled", () => {
const p = fakeProps();
p.env = { camera: "NONE" };
const wrapper = shallow(<WeedDetector {...p} />);
const btn = wrapper.find("button").first();
expect(btn.props().title).toEqual(Content.NO_CAMERA_SELECTED);
btn.simulate("click");
expect(error).toHaveBeenCalledWith(
ToolTips.SELECT_A_CAMERA, Content.NO_CAMERA_SELECTED);
expect(detectPlants).not.toHaveBeenCalled();
});
it("executes clear weeds", () => {
const wrapper =
shallow<WeedDetector>(<WeedDetector {...fakeProps()} />);
const wrapper = shallow<WeedDetector>(<WeedDetector {...fakeProps()} />);
expect(wrapper.instance().state.deletionProgress).toBeUndefined();
clickButton(wrapper, 1, "clear weeds");
expect(deletePoints).toHaveBeenCalledWith(
"weeds", { created_by: "plant-detection" }, expect.any(Function));
expect(wrapper.instance().state.deletionProgress).toEqual("Deleting...");
const fakeProgress = { completed: 50, total: 100, isDone: false };
mockDeletePoints.mock.calls[0][2](fakeProgress);
expect(wrapper.instance().state.deletionProgress).toEqual("50 %");
fakeProgress.isDone = true;
mockDeletePoints.mock.calls[0][2](fakeProgress);
expect(wrapper.instance().state.deletionProgress).toEqual("");
});
it("saves changes", () => {
@ -85,16 +114,9 @@ describe("<WeedDetector />", () => {
});
it("calls scanImage", () => {
const p = fakeProps();
p.dispatch = jest.fn(x => x());
const wrapper = shallow(<WeedDetector {...p} />);
const wrapper = shallow(<WeedDetector {...fakeProps()} />);
wrapper.find("ImageWorkspace").simulate("processPhoto", 1);
expect(mockDevice.execScript).toHaveBeenCalledWith(
"historical-plant-detection",
[expect.objectContaining({
kind: "pair",
args: expect.objectContaining({ value: "1" })
})]);
expect(scanImage).toHaveBeenCalledWith(1);
});
it("calls selectImage", () => {

View File

@ -69,7 +69,7 @@ export function scanImage(imageId: number) {
};
}
export function test() {
export function detectPlants() {
return function () {
getDevice().execScript("plant-detection").catch(() => { });
};

View File

@ -154,10 +154,9 @@ export class ImageWorkspace extends React.Component<ImageWorkspaceProps, {}> {
<Col xs={12}>
<button
className="green fb-button"
title="Scan this image"
title={t("Scan this image")}
onClick={this.maybeProcessPhoto}
disabled={!this.props.botOnline}
hidden={!this.props.images.length}>
disabled={!this.props.botOnline || !this.props.images.length}>
{t("Scan image")}
</button>
</Col>

View File

@ -1,7 +1,7 @@
import * as React from "react";
import { DetectorState } from "./interfaces";
import { Row, Col } from "../../ui/index";
import { deletePoints, scanImage, test } from "./actions";
import { deletePoints, scanImage, detectPlants } from "./actions";
import { selectImage } from "../images/actions";
import { Progress } from "../../util";
import { FarmwareProps, Feature } from "../../devices/interfaces";
@ -11,6 +11,9 @@ import { envGet } from "./remote_env/selectors";
import { MustBeOnline, isBotOnline } from "../../devices/must_be_online";
import { envSave } from "./remote_env/actions";
import { t } from "../../i18next_wrapper";
import {
cameraBtnProps
} from "../../devices/components/fbos_settings/camera_selection";
export const namespace = (prefix: string) => (key: string): WDENVKey => {
const namespacedKey = prefix + key;
@ -52,6 +55,8 @@ export class WeedDetector
: envSave(key, value)
render() {
const wDEnvGet = (key: WDENVKey) => envGet(key, this.props.wDEnv);
const camDisabled = cameraBtnProps(this.props.env);
return <div className="weed-detector">
<div className="farmware-button">
<MustBeOnline
@ -60,8 +65,9 @@ export class WeedDetector
hideBanner={true}
lockOpen={process.env.NODE_ENV !== "production"}>
<button
onClick={this.props.dispatch(test)}
className="fb-button green">
className={`fb-button green ${camDisabled.class}`}
title={camDisabled.title}
onClick={camDisabled.click || this.props.dispatch(detectPlants)}>
{t("detect weeds")}
</button>
</MustBeOnline>
@ -86,15 +92,15 @@ export class WeedDetector
images={this.props.images}
onChange={this.change}
timeSettings={this.props.timeSettings}
iteration={envGet(this.namespace("iteration"), this.props.env)}
morph={envGet(this.namespace("morph"), this.props.env)}
blur={envGet(this.namespace("blur"), this.props.env)}
H_LO={envGet(this.namespace("H_LO"), this.props.env)}
H_HI={envGet(this.namespace("H_HI"), this.props.env)}
S_LO={envGet(this.namespace("S_LO"), this.props.env)}
S_HI={envGet(this.namespace("S_HI"), this.props.env)}
V_LO={envGet(this.namespace("V_LO"), this.props.env)}
V_HI={envGet(this.namespace("V_HI"), this.props.env)} />
iteration={wDEnvGet(this.namespace("iteration"))}
morph={wDEnvGet(this.namespace("morph"))}
blur={wDEnvGet(this.namespace("blur"))}
H_LO={wDEnvGet(this.namespace("H_LO"))}
H_HI={wDEnvGet(this.namespace("H_HI"))}
S_LO={wDEnvGet(this.namespace("S_LO"))}
S_HI={wDEnvGet(this.namespace("S_HI"))}
V_LO={wDEnvGet(this.namespace("V_LO"))}
V_HI={wDEnvGet(this.namespace("V_HI"))} />
</MustBeOnline>
</Col>
</Row>

View File

@ -1,21 +1,16 @@
import { Everything } from "../interfaces";
import {
selectAllLogs, maybeGetTimeSettings, maybeGetDevice
} from "../resources/selectors";
import { selectAllLogs, maybeGetTimeSettings } from "../resources/selectors";
import { LogsProps } from "./interfaces";
import {
sourceFbosConfigValue
} from "../devices/components/source_config_value";
import {
validFbosConfig, determineInstalledOsVersion,
createShouldDisplayFn as shouldDisplayFunc
} from "../util";
import { validFbosConfig } from "../util";
import { ResourceIndex } from "../resources/interfaces";
import { TaggedLog } from "farmbot";
import { getWebAppConfigValue } from "../config_storage/actions";
import { getFbosConfig } from "../resources/getters";
import { chain } from "lodash";
import { DevSettings } from "../account/dev/dev_support";
import { getShouldDisplayFn } from "../farmware/state_to_props";
/** Take the specified number of logs after sorting by time created. */
export function takeSortedLogs(
@ -30,19 +25,12 @@ export function takeSortedLogs(
export function mapStateToProps(props: Everything): LogsProps {
const { hardware } = props.bot;
const fbosConfig = validFbosConfig(getFbosConfig(props.resources.index));
const sourceFbosConfig =
sourceFbosConfigValue(fbosConfig, hardware.configuration);
const installedOsVersion = determineInstalledOsVersion(
props.bot, maybeGetDevice(props.resources.index));
const fbosVersionOverride = DevSettings.overriddenFbosVersion();
const shouldDisplay = shouldDisplayFunc(
installedOsVersion, props.bot.minOsFeatureData, fbosVersionOverride);
return {
dispatch: props.dispatch,
sourceFbosConfig,
sourceFbosConfig: sourceFbosConfigValue(fbosConfig, hardware.configuration),
logs: takeSortedLogs(250, props.resources.index),
timeSettings: maybeGetTimeSettings(props.resources.index),
getConfigValue: getWebAppConfigValue(() => props),
shouldDisplay,
shouldDisplay: getShouldDisplayFn(props.resources.index, props.bot),
};
}

View File

@ -9,21 +9,16 @@ import {
maybeGetRegimen,
findId,
findSequence,
maybeGetDevice,
findSequenceById,
maybeGetTimeSettings
} from "../resources/selectors";
import { TaggedRegimen, TaggedSequence } from "farmbot";
import moment from "moment";
import { ResourceIndex, UUID, VariableNameSet } from "../resources/interfaces";
import {
randomColor, determineInstalledOsVersion,
createShouldDisplayFn as shouldDisplayFunc,
timeFormatString
} from "../util";
import { randomColor, timeFormatString } from "../util";
import { resourceUsageList } from "../resources/in_use";
import { groupBy, chain, sortBy } from "lodash";
import { DevSettings } from "../account/dev/dev_support";
import { getShouldDisplayFn } from "../farmware/state_to_props";
export function mapStateToProps(props: Everything): Props {
const { resources, dispatch, bot } = props;
@ -36,12 +31,6 @@ export function mapStateToProps(props: Everything): Props {
const calendar = current ?
generateCalendar(current, index, dispatch, timeSettings) : [];
const installedOsVersion = determineInstalledOsVersion(
props.bot, maybeGetDevice(props.resources.index));
const fbosVersionOverride = DevSettings.overriddenFbosVersion();
const shouldDisplay = shouldDisplayFunc(
installedOsVersion, props.bot.minOsFeatureData, fbosVersionOverride);
const calledSequences = (): UUID[] => {
if (current) {
const sequenceIds = current.body.regimen_items.map(x => x.sequence_id);
@ -70,7 +59,7 @@ export function mapStateToProps(props: Everything): Props {
bot,
calendar,
regimenUsageStats: resourceUsageList(props.resources.index.inUse),
shouldDisplay,
shouldDisplay: getShouldDisplayFn(props.resources.index, props.bot),
schedulerOpen,
};
}

View File

@ -43,8 +43,8 @@ import {
import { fakeSequence } from "../../__test_support__/fake_state/resources";
import { destroy, save, edit } from "../../api/crud";
import {
fakeHardwareFlags
} from "../../__test_support__/sequence_hardware_settings";
fakeHardwareFlags, fakeFarmwareData as fakeFarmwareData
} from "../../__test_support__/fake_sequence_step_data";
import { SpecialStatus } from "farmbot";
import { move, splice } from "../step_tiles";
import { copySequence, editCurrentSequence } from "../actions";
@ -66,12 +66,7 @@ describe("<SequenceEditorMiddleActive/>", () => {
resources: buildResourceIndex(FAKE_RESOURCES).index,
syncStatus: "synced",
hardwareFlags: fakeHardwareFlags(),
farmwareInfo: {
farmwareNames: [],
firstPartyFarmwareNames: [],
showFirstPartyFarmware: false,
farmwareConfigs: {},
},
farmwareData: fakeFarmwareData(),
shouldDisplay: jest.fn(),
getWebAppConfigValue: jest.fn(),
menuOpen: false,

View File

@ -7,8 +7,8 @@ import {
} from "../../__test_support__/resource_index_builder";
import { fakeSequence } from "../../__test_support__/fake_state/resources";
import {
fakeHardwareFlags
} from "../../__test_support__/sequence_hardware_settings";
fakeHardwareFlags, fakeFarmwareData
} from "../../__test_support__/fake_sequence_step_data";
describe("<SequenceEditorMiddle/>", () => {
function fakeProps(): SequenceEditorMiddleProps {
@ -18,12 +18,7 @@ describe("<SequenceEditorMiddle/>", () => {
resources: buildResourceIndex(FAKE_RESOURCES).index,
syncStatus: "synced",
hardwareFlags: fakeHardwareFlags(),
farmwareInfo: {
farmwareNames: [],
firstPartyFarmwareNames: [],
showFirstPartyFarmware: false,
farmwareConfigs: {},
},
farmwareData: fakeFarmwareData(),
shouldDisplay: jest.fn(),
getWebAppConfigValue: jest.fn(),
menuOpen: false,

View File

@ -15,8 +15,8 @@ import {
import { fakeSequence } from "../../__test_support__/fake_state/resources";
import { ToolTips, Actions } from "../../constants";
import {
fakeHardwareFlags
} from "../../__test_support__/sequence_hardware_settings";
fakeHardwareFlags, fakeFarmwareData
} from "../../__test_support__/fake_sequence_step_data";
import { push } from "../../history";
import { mapStateToFolderProps } from "../../folders/map_state_to_props";
import { fakeState } from "../../__test_support__/fake_state";
@ -29,12 +29,7 @@ describe("<Sequences/>", () => {
resources: buildResourceIndex(FAKE_RESOURCES).index,
syncStatus: "synced",
hardwareFlags: fakeHardwareFlags(),
farmwareInfo: {
farmwareNames: [],
firstPartyFarmwareNames: [],
showFirstPartyFarmware: false,
farmwareConfigs: {},
},
farmwareData: fakeFarmwareData(),
shouldDisplay: jest.fn(),
getWebAppConfigValue: jest.fn(),
menuOpen: false,

View File

@ -4,31 +4,25 @@ const mockData = {
fakeSequences: [fakeSequence()]
};
jest.mock("../../util/urls", () => {
return {
urlFriendly: jest.fn(x => x),
lastUrlChunk: jest.fn(() => mockData.lastUrlChunk)
};
});
jest.mock("../../util/urls", () => ({
urlFriendly: jest.fn(x => x),
lastUrlChunk: jest.fn(() => mockData.lastUrlChunk)
}));
jest.mock("../actions", () => ({ selectSequence: jest.fn() }));
jest.mock("../../resources/selectors", () => {
return {
selectAllSequences: jest.fn(() => {
return mockData.fakeSequences || [];
})
};
});
jest.mock("../../resources/selectors", () => ({
selectAllSequences: jest.fn(() => mockData.fakeSequences || []),
}));
jest.mock("../../redux/store", () => {
return {
store: {
dispatch: jest.fn(),
getState: jest.fn(() => ({ resources: { index: {} } }))
}
};
});
jest.mock("../../redux/store", () => ({
store: {
dispatch: jest.fn(),
getState: jest.fn(() => ({ resources: { index: {} } }))
}
}));
jest.mock("../../account/dev/dev_support", () => ({}));
import { setActiveSequenceByName } from "../set_active_sequence_by_name";
import { selectSequence } from "../actions";

View File

@ -3,7 +3,7 @@ import { fakeState } from "../../__test_support__/fake_state";
import { Feature } from "../../devices/interfaces";
import { fakeFarmwareManifestV1 } from "../../__test_support__/fake_farmwares";
import {
fakeSequence, fakeWebAppConfig
fakeSequence, fakeWebAppConfig, fakeFarmwareEnv
} from "../../__test_support__/fake_state/resources";
import {
buildResourceIndex
@ -67,12 +67,24 @@ describe("mapStateToProps()", () => {
"My Fake Farmware": fakeFarmwareManifestV1()
};
const props = mapStateToProps(state);
expect(props.farmwareInfo.farmwareNames).toEqual(["My Fake Farmware"]);
expect(props.farmwareInfo.showFirstPartyFarmware).toEqual(true);
expect(props.farmwareInfo.farmwareConfigs).toEqual({
expect(props.farmwareData.farmwareNames).toEqual(["My Fake Farmware"]);
expect(props.farmwareData.showFirstPartyFarmware).toEqual(true);
expect(props.farmwareData.farmwareConfigs).toEqual({
"My Fake Farmware": [{
name: "config_1", label: "Config 1", value: "4"
}]
});
});
it("returns api props", () => {
const state = fakeState();
const fakeEnv = fakeFarmwareEnv();
fakeEnv.body.key = "camera";
fakeEnv.body.value = "NONE";
state.resources = buildResourceIndex([fakeEnv]);
state.bot.minOsFeatureData = { api_farmware_env: "8.0.0" };
state.bot.hardware.informational_settings.controller_version = "8.0.0";
const props = mapStateToProps(state);
expect(props.farmwareData.cameraDisabled).toEqual(true);
});
});

View File

@ -6,7 +6,7 @@ import { StepDragger } from "../draggable/step_dragger";
import { renderCeleryNode } from "./step_tiles/index";
import { ResourceIndex } from "../resources/interfaces";
import { getStepTag } from "../resources/sequence_tagging";
import { HardwareFlags, FarmwareInfo } from "./interfaces";
import { HardwareFlags, FarmwareData } from "./interfaces";
import { ShouldDisplay } from "../devices/interfaces";
import { AddCommandButton } from "./sequence_editor_middle_active";
import { ErrorBoundary } from "../error_boundary";
@ -18,7 +18,7 @@ export interface AllStepsProps {
dispatch: Function;
resources: ResourceIndex;
hardwareFlags?: HardwareFlags;
farmwareInfo?: FarmwareInfo;
farmwareData?: FarmwareData;
shouldDisplay?: ShouldDisplay;
confirmStepDeletion: boolean;
showPins?: boolean;
@ -43,7 +43,7 @@ export class AllSteps extends React.Component<AllStepsProps, {}> {
currentSequence: sequence,
resources: this.props.resources,
hardwareFlags: this.props.hardwareFlags,
farmwareInfo: this.props.farmwareInfo,
farmwareData: this.props.farmwareData,
shouldDisplay: this.props.shouldDisplay,
confirmStepDeletion: this.props.confirmStepDeletion,
showPins: this.props.showPins,

View File

@ -44,7 +44,7 @@ export interface Props {
resources: ResourceIndex;
syncStatus: SyncStatus;
hardwareFlags: HardwareFlags;
farmwareInfo: FarmwareInfo;
farmwareData: FarmwareData;
shouldDisplay: ShouldDisplay;
getWebAppConfigValue: GetWebAppConfigValue;
menuOpen: boolean;
@ -58,7 +58,7 @@ export interface SequenceEditorMiddleProps {
resources: ResourceIndex;
syncStatus: SyncStatus;
hardwareFlags: HardwareFlags;
farmwareInfo: FarmwareInfo;
farmwareData: FarmwareData;
shouldDisplay: ShouldDisplay;
getWebAppConfigValue: GetWebAppConfigValue;
menuOpen: boolean;
@ -189,11 +189,12 @@ export type dispatcher = (a: Function | { type: string }) => DataXferObj;
export type FarmwareConfigs = { [x: string]: FarmwareConfig[] };
export interface FarmwareInfo {
export interface FarmwareData {
farmwareNames: string[];
firstPartyFarmwareNames: string[];
showFirstPartyFarmware: boolean;
farmwareConfigs: FarmwareConfigs;
cameraDisabled: boolean;
}
export interface StepParams {
@ -203,7 +204,7 @@ export interface StepParams {
index: number;
resources: ResourceIndex;
hardwareFlags?: HardwareFlags;
farmwareInfo?: FarmwareInfo;
farmwareData?: FarmwareData;
shouldDisplay?: ShouldDisplay;
confirmStepDeletion: boolean;
showPins?: boolean;

View File

@ -23,7 +23,7 @@ export class SequenceEditorMiddle
resources={this.props.resources}
syncStatus={this.props.syncStatus}
hardwareFlags={this.props.hardwareFlags}
farmwareInfo={this.props.farmwareInfo}
farmwareData={this.props.farmwareData}
shouldDisplay={this.props.shouldDisplay}
getWebAppConfigValue={this.props.getWebAppConfigValue}
menuOpen={this.props.menuOpen} />}

View File

@ -249,7 +249,7 @@ export class SequenceEditorMiddleActive extends
dispatch: this.props.dispatch,
resources: this.props.resources,
hardwareFlags: this.props.hardwareFlags,
farmwareInfo: this.props.farmwareInfo,
farmwareData: this.props.farmwareData,
shouldDisplay: this.props.shouldDisplay,
confirmStepDeletion: !!getConfig(BooleanSetting.confirm_step_deletion),
showPins: !!getConfig(BooleanSetting.show_pins),

View File

@ -58,7 +58,7 @@ export class RawSequences extends React.Component<Props, {}> {
sequence={this.props.sequence}
resources={this.props.resources}
hardwareFlags={this.props.hardwareFlags}
farmwareInfo={this.props.farmwareInfo}
farmwareData={this.props.farmwareData}
shouldDisplay={this.props.shouldDisplay}
getWebAppConfigValue={this.props.getWebAppConfigValue}
menuOpen={this.props.menuOpen} />

View File

@ -1,22 +1,20 @@
import { Everything } from "../interfaces";
import { Props, HardwareFlags, FarmwareConfigs } from "./interfaces";
import {
selectAllSequences, findSequence, maybeGetDevice
} from "../resources/selectors";
import { selectAllSequences, findSequence } from "../resources/selectors";
import { getStepTag } from "../resources/sequence_tagging";
import { enabledAxisMap } from "../devices/components/axis_tracking_status";
import {
createShouldDisplayFn as shouldDisplayFunc,
determineInstalledOsVersion, validFwConfig
} from "../util";
import { validFwConfig } from "../util";
import { BooleanSetting } from "../session_keys";
import { getWebAppConfigValue } from "../config_storage/actions";
import { getFirmwareConfig } from "../resources/getters";
import { Farmwares } from "../farmware/interfaces";
import { manifestInfo } from "../farmware/generate_manifest_info";
import { DevSettings } from "../account/dev/dev_support";
import { calculateAxialLengths } from "../controls/move/direction_axes_props";
import { mapStateToFolderProps } from "../folders/map_state_to_props";
import { getEnv, getShouldDisplayFn } from "../farmware/state_to_props";
import {
cameraDisabled
} from "../devices/components/fbos_settings/camera_selection";
export function mapStateToProps(props: Everything): Props {
const uuid = props.resources.consumers.sequences.current;
@ -62,11 +60,8 @@ export function mapStateToProps(props: Everything): Props {
const farmwareConfigs: FarmwareConfigs = {};
Object.values(farmwares).map(fw => farmwareConfigs[fw.name] = fw.config);
const installedOsVersion = determineInstalledOsVersion(
props.bot, maybeGetDevice(props.resources.index));
const fbosVersionOverride = DevSettings.overriddenFbosVersion();
const shouldDisplay = shouldDisplayFunc(
installedOsVersion, props.bot.minOsFeatureData, fbosVersionOverride);
const shouldDisplay = getShouldDisplayFn(props.resources.index, props.bot);
const env = getEnv(props.resources.index, shouldDisplay, props.bot);
return {
dispatch: props.dispatch,
@ -79,11 +74,12 @@ export function mapStateToProps(props: Everything): Props {
.informational_settings
.sync_status || "unknown"),
hardwareFlags: hardwareFlags(),
farmwareInfo: {
farmwareData: {
farmwareNames,
firstPartyFarmwareNames,
showFirstPartyFarmware,
farmwareConfigs,
cameraDisabled: cameraDisabled(env),
},
shouldDisplay,
getWebAppConfigValue: getConfig,

View File

@ -6,7 +6,7 @@ import { emptyState } from "../../../resources/reducer";
import { HardwareFlags } from "../../interfaces";
import {
fakeHardwareFlags
} from "../../../__test_support__/sequence_hardware_settings";
} from "../../../__test_support__/fake_sequence_step_data";
describe("<TileCalibrate/>", () => {
const fakeProps = (): CalibrateParams => ({

View File

@ -4,8 +4,11 @@ import { mount, shallow } from "enzyme";
import { fakeSequence } from "../../../__test_support__/fake_state/resources";
import { ExecuteScript } from "farmbot/dist";
import { StepParams } from "../../interfaces";
import { Actions } from "../../../constants";
import { Actions, Content } from "../../../constants";
import { emptyState } from "../../../resources/reducer";
import {
fakeFarmwareData
} from "../../../__test_support__/fake_sequence_step_data";
describe("<TileExecuteScript/>", () => {
const fakeProps = (): StepParams => {
@ -15,18 +18,17 @@ describe("<TileExecuteScript/>", () => {
label: "farmware-to-execute"
}
};
const farmwareData = fakeFarmwareData();
farmwareData.farmwareNames = ["one", "two", "three"];
farmwareData.firstPartyFarmwareNames = ["one"];
farmwareData.farmwareConfigs = { "farmware-to-execute": [] };
return {
currentSequence: fakeSequence(),
currentStep,
dispatch: jest.fn(),
index: 0,
resources: emptyState().index,
farmwareInfo: {
farmwareNames: ["one", "two", "three"],
firstPartyFarmwareNames: ["one"],
showFirstPartyFarmware: false,
farmwareConfigs: { "farmware-to-execute": [] },
},
farmwareData,
confirmStepDeletion: false,
};
};
@ -61,7 +63,7 @@ describe("<TileExecuteScript/>", () => {
it("shows 1st party in list", () => {
const p = fakeProps();
p.farmwareInfo && (p.farmwareInfo.showFirstPartyFarmware = true);
p.farmwareData && (p.farmwareData.showFirstPartyFarmware = true);
const wrapper = shallow(<TileExecuteScript {...p} />);
expect(wrapper.find("FBSelect").props().list).toEqual([
{ label: "one", value: "one" },
@ -81,7 +83,7 @@ describe("<TileExecuteScript/>", () => {
it("shows special 1st-party Farmware name", () => {
const p = fakeProps();
(p.currentStep as ExecuteScript).args.label = "plant-detection";
p.farmwareInfo && p.farmwareInfo.farmwareNames.push("plant-detection");
p.farmwareData && p.farmwareData.farmwareNames.push("plant-detection");
const wrapper = mount(<TileExecuteScript {...p} />);
expect(wrapper.find("label").length).toEqual(1);
expect(wrapper.text()).toContain("Weed Detector");
@ -89,7 +91,7 @@ describe("<TileExecuteScript/>", () => {
it("renders manual input", () => {
const p = fakeProps();
p.farmwareInfo = undefined;
p.farmwareData = undefined;
const wrapper = mount(<TileExecuteScript {...p} />);
expect(wrapper.find("button").text()).toEqual("Manual Input");
expect(wrapper.find("label").at(1).text()).toEqual("Manual input");
@ -131,4 +133,12 @@ describe("<TileExecuteScript/>", () => {
type: Actions.OVERWRITE_RESOURCE
});
});
it("displays warning when camera is disabled", () => {
const p = fakeProps();
(p.currentStep as ExecuteScript).args.label = "plant-detection";
p.farmwareData && (p.farmwareData.cameraDisabled = true);
const wrapper = mount(<TileExecuteScript {...p} />);
expect(wrapper.text()).toContain(Content.NO_CAMERA_SELECTED);
});
});

View File

@ -4,7 +4,7 @@ import { mount } from "enzyme";
import { fakeSequence } from "../../../__test_support__/fake_state/resources";
import {
fakeHardwareFlags
} from "../../../__test_support__/sequence_hardware_settings";
} from "../../../__test_support__/fake_sequence_step_data";
import { HardwareFlags } from "../../interfaces";
import { emptyState } from "../../../resources/reducer";

View File

@ -3,7 +3,7 @@ import { mount } from "enzyme";
import { MoveAbsoluteWarningProps } from "../../interfaces";
import {
fakeHardwareFlags
} from "../../../__test_support__/sequence_hardware_settings";
} from "../../../__test_support__/fake_sequence_step_data";
import { MoveAbsoluteWarning } from "../tile_move_absolute_conflict_check";
describe("<MoveAbsoluteWarning/>", () => {

View File

@ -13,7 +13,7 @@ import {
} from "farmbot";
import {
fakeHardwareFlags
} from "../../../__test_support__/sequence_hardware_settings";
} from "../../../__test_support__/fake_sequence_step_data";
import { emptyState } from "../../../resources/reducer";
import { inputEvent } from "../../../__test_support__/fake_input_event";
import { StepParams } from "../../interfaces";

View File

@ -5,6 +5,10 @@ import { fakeSequence } from "../../../__test_support__/fake_state/resources";
import { TakePhoto } from "farmbot/dist";
import { StepParams } from "../../interfaces";
import { emptyState } from "../../../resources/reducer";
import {
fakeFarmwareData
} from "../../../__test_support__/fake_sequence_step_data";
import { Content } from "../../../constants";
describe("<TileTakePhoto/>", () => {
const currentStep: TakePhoto = {
@ -19,6 +23,7 @@ describe("<TileTakePhoto/>", () => {
index: 0,
resources: emptyState().index,
confirmStepDeletion: false,
farmwareData: fakeFarmwareData(),
});
it("renders step", () => {
@ -34,4 +39,11 @@ describe("<TileTakePhoto/>", () => {
expect(inputs.first().props().placeholder).toEqual("Take a Photo");
expect(wrapper.text()).toContain("farmware page");
});
it("displays warning when camera is disabled", () => {
const p = fakeProps();
p.farmwareData && (p.farmwareData.cameraDisabled = true);
const wrapper = mount(<TileTakePhoto {...p} />);
expect(wrapper.text()).toContain(Content.NO_CAMERA_SELECTED);
});
});

View File

@ -1,8 +1,10 @@
import * as React from "react";
import { StepParams } from "../interfaces";
import { ToolTips } from "../../constants";
import { ToolTips, Content } from "../../constants";
import { StepInputBox } from "../inputs/step_input_box";
import { StepWrapper, StepHeader, StepContent } from "../step_ui/index";
import {
StepWrapper, StepHeader, StepContent, StepWarning
} from "../step_ui/index";
import { Row, Col, FBSelect, DropDownItem } from "../../ui/index";
import { editStep } from "../../api/crud";
import { ExecuteScript, FarmwareConfig } from "farmbot";
@ -10,14 +12,14 @@ import { FarmwareInputs, farmwareList } from "./tile_execute_script_support";
import { t } from "../../i18next_wrapper";
export function TileExecuteScript(props: StepParams) {
const { dispatch, currentStep, index, currentSequence, farmwareInfo } = props;
const { dispatch, currentStep, index, currentSequence, farmwareData } = props;
if (currentStep.kind === "execute_script") {
const farmwareName = currentStep.args.label;
/** Selected Farmware is installed on connected bot. */
const isInstalled = (name: string): boolean => {
return !!(farmwareInfo && farmwareInfo.farmwareNames.includes(name));
return !!(farmwareData && farmwareData.farmwareNames.includes(name));
};
const selectedFarmwareDDI = (name: string): DropDownItem => {
@ -52,8 +54,8 @@ export function TileExecuteScript(props: StepParams) {
/** Configs (inputs) from Farmware manifest for <FarmwareInputs />. */
const currentFarmwareConfigDefaults = (fwName: string): FarmwareConfig[] => {
return farmwareInfo && farmwareInfo.farmwareConfigs[fwName]
? farmwareInfo.farmwareConfigs[fwName]
return farmwareData && farmwareData.farmwareConfigs[fwName]
? farmwareData.farmwareConfigs[fwName]
: [];
};
@ -66,14 +68,20 @@ export function TileExecuteScript(props: StepParams) {
currentStep={currentStep}
dispatch={dispatch}
index={index}
confirmStepDeletion={props.confirmStepDeletion} />
confirmStepDeletion={props.confirmStepDeletion}>
{props.farmwareData && props.farmwareData.cameraDisabled &&
(farmwareName === "plant-detection") &&
<StepWarning
titleBase={t(Content.NO_CAMERA_SELECTED)}
warning={t(ToolTips.SELECT_A_CAMERA)} />}
</StepHeader>
<StepContent className={className}>
<Row>
<Col xs={12}>
<label>{t("Package Name")}</label>
<FBSelect
key={JSON.stringify(props.currentSequence)}
list={farmwareList(farmwareInfo)}
list={farmwareList(farmwareData)}
selectedItem={selectedFarmwareDDI(farmwareName)}
onChange={updateStepFarmwareSelection}
allowEmpty={true}

View File

@ -1,5 +1,5 @@
import * as React from "react";
import { FarmwareInfo } from "../interfaces";
import { FarmwareData } from "../interfaces";
import { DropDownItem, BlurableInput, Help } from "../../ui/index";
import { without, isNumber } from "lodash";
import { ExecuteScript, Pair, FarmwareConfig } from "farmbot";
@ -113,11 +113,11 @@ const farmwareInputs =
/** List of installed Farmware, if bot is connected (for DropDown). */
export const farmwareList =
(farmwareInfo: FarmwareInfo | undefined): DropDownItem[] => {
if (farmwareInfo) {
(farmwareData: FarmwareData | undefined): DropDownItem[] => {
if (farmwareData) {
const {
farmwareNames, showFirstPartyFarmware, firstPartyFarmwareNames
} = farmwareInfo;
} = farmwareData;
return farmwareNames
.filter(x => (firstPartyFarmwareNames && !showFirstPartyFarmware)
? !firstPartyFarmwareNames.includes(x) : x)

View File

@ -1,7 +1,7 @@
import * as React from "react";
import { StepParams } from "../interfaces";
import { ToolTips } from "../../constants";
import { StepWrapper, StepHeader, StepContent } from "../step_ui";
import { ToolTips, Content } from "../../constants";
import { StepWrapper, StepHeader, StepContent, StepWarning } from "../step_ui";
import { Col, Row } from "../../ui/index";
import { Link } from "../../link";
import { t } from "../../i18next_wrapper";
@ -17,7 +17,12 @@ export function TileTakePhoto(props: StepParams) {
currentStep={currentStep}
dispatch={dispatch}
index={index}
confirmStepDeletion={props.confirmStepDeletion} />
confirmStepDeletion={props.confirmStepDeletion}>
{props.farmwareData && props.farmwareData.cameraDisabled &&
<StepWarning
titleBase={t(Content.NO_CAMERA_SELECTED)}
warning={t(ToolTips.SELECT_A_CAMERA)} />}
</StepHeader>
<StepContent className={className}>
<Row>
<Col xs={12}>

View File

@ -6,6 +6,7 @@ import { t } from "../../i18next_wrapper";
interface StepWarningProps {
warning: string;
conflicts?: Record<Xyz, boolean>;
titleBase?: string;
}
const TITLE_BASE = t("Hardware setting conflict");
@ -20,11 +21,10 @@ export const conflictsString = (conflicts: Record<Xyz, boolean>) => {
};
export function StepWarning(props: StepWarningProps) {
const { conflicts, warning } = props;
const { conflicts, warning, titleBase } = props;
const warningTitle = () => {
return conflicts
? TITLE_BASE + ": " + conflictsString(conflicts)
: TITLE_BASE;
return (titleBase || TITLE_BASE) +
(conflicts ? ": " + conflictsString(conflicts) : "");
};
return <div className="step-warning">
<Popover