model and version updates

pull/1698/head
gabrielburnworth 2020-02-15 10:29:31 -08:00
parent 9dab0c4bc5
commit cf0af59e42
68 changed files with 708 additions and 499 deletions

View File

@ -27,7 +27,7 @@ module Devices
add_tool_slot(name: ToolNames::SEED_TROUGH_1,
x: 0,
y: 25,
z: -200,
z: 0,
tool: tools_seed_trough_1,
pullout_direction: ToolSlot::NONE,
gantry_mounted: true)
@ -37,25 +37,18 @@ module Devices
add_tool_slot(name: ToolNames::SEED_TROUGH_2,
x: 0,
y: 50,
z: -200,
z: 0,
tool: tools_seed_trough_2,
pullout_direction: ToolSlot::NONE,
gantry_mounted: true)
end
def tool_slots_slot_3
add_tool_slot(name: ToolNames::SEED_TROUGH_3,
x: 0,
y: 75,
z: -200,
tool: tools_seed_trough_3,
pullout_direction: ToolSlot::NONE,
gantry_mounted: true)
end
def tool_slots_slot_3; end
def tool_slots_slot_4; end
def tool_slots_slot_5; end
def tool_slots_slot_6; end
def tool_slots_slot_7; end
def tool_slots_slot_8; end
def tools_seed_bin; end
def tools_seed_tray; end
@ -69,11 +62,6 @@ module Devices
add_tool(ToolNames::SEED_TROUGH_2)
end
def tools_seed_trough_3
@tools_seed_trough_3 ||=
add_tool(ToolNames::SEED_TROUGH_3)
end
def tools_seeder; end
def tools_soil_sensor; end
def tools_watering_nozzle; end

View File

@ -75,6 +75,9 @@ module Devices
tool: tools_weeder)
end
def tool_slots_slot_7; end
def tool_slots_slot_8; end
def tools_seed_bin
@tools_seed_bin ||=
add_tool(ToolNames::SEED_BIN)
@ -87,7 +90,6 @@ module Devices
def tools_seed_trough_1; end
def tools_seed_trough_2; end
def tools_seed_trough_3; end
def tools_seeder
@tools_seeder ||=

View File

@ -37,7 +37,6 @@ module Devices
:tools_seed_tray,
:tools_seed_trough_1,
:tools_seed_trough_2,
:tools_seed_trough_3,
:tools_seeder,
:tools_soil_sensor,
:tools_watering_nozzle,
@ -50,6 +49,8 @@ module Devices
:tool_slots_slot_4,
:tool_slots_slot_5,
:tool_slots_slot_6,
:tool_slots_slot_7,
:tool_slots_slot_8,
# WEBCAM FEEDS ===========================
:webcam_feeds,
@ -152,11 +153,12 @@ module Devices
def tool_slots_slot_4; end
def tool_slots_slot_5; end
def tool_slots_slot_6; end
def tool_slots_slot_7; end
def tool_slots_slot_8; end
def tools_seed_bin; end
def tools_seed_tray; end
def tools_seed_trough_1; end
def tools_seed_trough_2; end
def tools_seed_trough_3; end
def tools_seeder; end
def tools_soil_sensor; end
def tools_watering_nozzle; end

View File

@ -31,7 +31,6 @@ module Devices
LIGHTING = "Lighting"
SEED_TROUGH_1 = "Seed Trough 1"
SEED_TROUGH_2 = "Seed Trough 2"
SEED_TROUGH_3 = "Seed Trough 3"
end
# Stub plants ==============================

View File

@ -6,6 +6,36 @@ module Devices
.fbos_config
.update!(firmware_hardware: FbosConfig::FARMDUINO_K15)
end
def tool_slots_slot_7
add_tool_slot(name: ToolNames::SEED_TROUGH_1,
x: 0,
y: 25,
z: 0,
tool: tools_seed_trough_1,
pullout_direction: ToolSlot::NONE,
gantry_mounted: true)
end
def tool_slots_slot_8
add_tool_slot(name: ToolNames::SEED_TROUGH_2,
x: 0,
y: 50,
z: 0,
tool: tools_seed_trough_2,
pullout_direction: ToolSlot::NONE,
gantry_mounted: true)
end
def tools_seed_trough_1
@tools_seed_trough_1 ||=
add_tool(ToolNames::SEED_TROUGH_1)
end
def tools_seed_trough_2
@tools_seed_trough_2 ||=
add_tool(ToolNames::SEED_TROUGH_2)
end
end
end
end

View File

@ -18,6 +18,36 @@ module Devices
def settings_default_map_size_y
device.web_app_config.update!(map_size_y: 2_900)
end
def tool_slots_slot_7
add_tool_slot(name: ToolNames::SEED_TROUGH_1,
x: 0,
y: 25,
z: 0,
tool: tools_seed_trough_1,
pullout_direction: ToolSlot::NONE,
gantry_mounted: true)
end
def tool_slots_slot_8
add_tool_slot(name: ToolNames::SEED_TROUGH_2,
x: 0,
y: 50,
z: 0,
tool: tools_seed_trough_2,
pullout_direction: ToolSlot::NONE,
gantry_mounted: true)
end
def tools_seed_trough_1
@tools_seed_trough_1 ||=
add_tool(ToolNames::SEED_TROUGH_1)
end
def tools_seed_trough_2
@tools_seed_trough_2 ||=
add_tool(ToolNames::SEED_TROUGH_2)
end
end
end
end

View File

@ -28,11 +28,12 @@ module Devices
def tool_slots_slot_4; end
def tool_slots_slot_5; end
def tool_slots_slot_6; end
def tool_slots_slot_7; end
def tool_slots_slot_8; end
def tools_seed_bin; end
def tools_seed_tray; end
def tools_seed_trough_1; end
def tools_seed_trough_2; end
def tools_seed_trough_3; end
def tools_seeder; end
def tools_soil_sensor; end
def tools_watering_nozzle; end

View File

@ -157,7 +157,6 @@ describe("mapStateToProps()", () => {
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" };

View File

@ -1,15 +1,9 @@
jest.mock("../../slow_down", () => {
return {
slowDown: jest.fn((fn: Function) => fn),
};
});
jest.mock("../../../devices/actions", () => ({
badVersion: jest.fn(),
EXPECTED_MAJOR: 1,
EXPECTED_MINOR: 0,
jest.mock("../../slow_down", () => ({
slowDown: jest.fn((fn: Function) => fn)
}));
jest.mock("../../../devices/actions", () => ({ badVersion: jest.fn() }));
import {
onStatus,
incomingStatus,
@ -49,8 +43,10 @@ describe("onStatus()", () => {
});
it("version ok", () => {
globalConfig.MINIMUM_FBOS_VERSION = "1.0.0";
callOnStatus("1.0.0");
expect(badVersion).not.toHaveBeenCalled();
delete globalConfig.MINIMUM_FBOS_VERSION;
});
});

View File

@ -8,13 +8,7 @@ import { success, error, info, warning, fun, busy } from "../toast/toast";
import { HardwareState } from "../devices/interfaces";
import { GetState, ReduxAction } from "../redux/interfaces";
import { Content, Actions } from "../constants";
import {
EXPECTED_MAJOR,
EXPECTED_MINOR,
commandOK,
badVersion,
commandErr
} from "../devices/actions";
import { commandOK, badVersion, commandErr } from "../devices/actions";
import { init } from "../api/crud";
import { AuthState } from "../auth/interfaces";
import { autoSync } from "./auto_sync";
@ -123,7 +117,7 @@ const setBothUp = () => bothUp();
const legacyChecks = (getState: GetState) => {
const { controller_version } = getState().bot.hardware.informational_settings;
if (HACKY_FLAGS.needVersionCheck && controller_version) {
const IS_OK = versionOK(controller_version, EXPECTED_MAJOR, EXPECTED_MINOR);
const IS_OK = versionOK(controller_version);
if (!IS_OK) { badVersion(); }
HACKY_FLAGS.needVersionCheck = false;
}

View File

@ -580,6 +580,7 @@
font-size: 1.4rem;
}
}
.tools-header,
.tool-slots-header {
display: flex;
margin-top: 1rem;
@ -624,6 +625,12 @@
}
}
.add-stock-tools {
.filter-search {
margin-bottom: 1rem;
button {
margin-top: 0.2rem;
}
}
ul {
font-size: 1.2rem;
padding-left: 1rem;

View File

@ -1322,6 +1322,12 @@ ul {
}
}
.boolean-camera-calibration-config {
input[type=checkbox] {
display: block;
}
}
.tour-list {
margin: auto;
max-width: 300px;
@ -1533,16 +1539,24 @@ textarea:focus {
box-shadow: 0 0 10px rgba(0,0,0,.2);
}
.sort-path-info-bar {
background: lightgray;
.sort-option-bar {
cursor: pointer;
font-size: 1.1rem;
margin-top: 0.25rem;
margin-bottom: 0.25rem;
white-space: nowrap;
line-height: 1.75rem;
&:hover {
background: darken(lightgray, 10%);
border: 2px solid $panel_light_blue;
&:hover, &.selected {
border: 2px solid $medium_gray;
border-radius: 2px;
.sort-path-info-bar {
background: darken($light_gray, 10%);
}
}
.sort-path-info-bar {
background: $light_gray;
font-size: 1.2rem;
padding-left: 0.5rem;
white-space: nowrap;
line-height: 2.5rem;
}
}

View File

@ -97,6 +97,7 @@
}
}
.camera-calibration,
.weed-detector{
.farmware-button{
position: relative;

View File

@ -35,7 +35,6 @@ describe("mapStateToProps()", () => {
it("uses the API as the source of FBOS settings", () => {
const fakeApiConfig = fakeFbosConfig();
fakeApiConfig.body.auto_sync = true;
fakeApiConfig.body.api_migrated = true;
mockFbosConfig = fakeApiConfig;
const props = mapStateToProps(fakeState());
expect(props.sourceFbosConfig("auto_sync")).toEqual({
@ -53,19 +52,6 @@ describe("mapStateToProps()", () => {
});
});
it("uses the bot as the source of FBOS settings: ignore API defaults", () => {
const state = fakeState();
state.bot.hardware.configuration.auto_sync = false;
const fakeApiConfig = fakeFbosConfig();
fakeApiConfig.body.auto_sync = true;
fakeApiConfig.body.api_migrated = false;
mockFbosConfig = fakeApiConfig;
const props = mapStateToProps(state);
expect(props.sourceFbosConfig("auto_sync")).toEqual({
value: false, consistent: true
});
});
it("returns API Farmware env vars", () => {
const state = fakeState();
state.bot.hardware.user_env = {};

View File

@ -26,8 +26,6 @@ import { t } from "../i18next_wrapper";
const ON = 1, OFF = 0;
export type ConfigKey = keyof McuParams;
export const EXPECTED_MAJOR = 6;
export const EXPECTED_MINOR = 0;
export const FEATURE_MIN_VERSIONS_URL =
"https://raw.githubusercontent.com/FarmBot/farmbot_os/staging/" +
"FEATURE_MIN_VERSIONS.json";
@ -132,7 +130,7 @@ export function sync(): Thunk {
return function (_dispatch, getState) {
const currentFBOSversion =
getState().bot.hardware.informational_settings.controller_version;
const IS_OK = versionOK(currentFBOSversion, EXPECTED_MAJOR, EXPECTED_MINOR);
const IS_OK = versionOK(currentFBOSversion);
if (IS_OK) {
getDevice()
.sync()

View File

@ -65,13 +65,7 @@ describe("<HardwareSettings />", () => {
it("shows param export menu", () => {
const p = fakeProps();
p.firmwareConfig = fakeFirmwareConfig().body;
p.firmwareConfig.api_migrated = true;
const wrapper = shallow(<HardwareSettings {...p} />);
expect(wrapper.html()).toContain("fa-download");
});
it("doesn't show param export menu", () => {
const wrapper = shallow(<HardwareSettings {...fakeProps()} />);
expect(wrapper.html()).not.toContain("fa-download");
});
});

View File

@ -4,7 +4,7 @@ import { t } from "../../i18next_wrapper";
import { FarmbotOsProps, FarmbotOsState, Feature } from "../interfaces";
import { Widget, WidgetHeader, WidgetBody, Row, Col } from "../../ui";
import { save, edit } from "../../api/crud";
import { MustBeOnline, isBotOnline } from "../must_be_online";
import { isBotOnline } from "../must_be_online";
import { Content } from "../../constants";
import { TimezoneSelector } from "../timezones/timezone_selector";
import { timezoneMismatch } from "../timezones/guess_timezone";
@ -116,53 +116,46 @@ export class FarmbotOsSettings
</div>
</Col>
</Row>
<MustBeOnline
syncStatus={sync_status}
networkState={this.props.botToMqttStatus}
lockOpen={process.env.NODE_ENV !== "production"
|| this.props.isValidFbosConfig}>
<CameraSelection
env={this.props.env}
botOnline={botOnline}
saveFarmwareEnv={this.props.saveFarmwareEnv}
shouldDisplay={this.props.shouldDisplay}
dispatch={this.props.dispatch} />
<BoardType
botOnline={botOnline}
bot={bot}
alerts={this.props.alerts}
dispatch={this.props.dispatch}
shouldDisplay={this.props.shouldDisplay}
timeSettings={this.props.timeSettings}
sourceFbosConfig={sourceFbosConfig} />
<AutoUpdateRow
shouldDisplay={this.props.shouldDisplay}
timeFormat={timeFormat}
device={this.props.deviceAccount}
dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig} />
<FarmbotOsRow
bot={this.props.bot}
osReleaseNotesHeading={this.osReleaseNotes.heading}
osReleaseNotes={this.osReleaseNotes.notes}
dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig}
shouldDisplay={this.props.shouldDisplay}
botOnline={botOnline}
botToMqttLastSeen={new Date(this.props.botToMqttLastSeen).getTime()}
timeSettings={this.props.timeSettings}
deviceAccount={this.props.deviceAccount} />
<AutoSyncRow
dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig} />
{this.props.shouldDisplay(Feature.boot_sequence) &&
<BootSequenceSelector />}
<PowerAndReset
controlPanelState={this.props.bot.controlPanelState}
dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig}
botOnline={botOnline} />
</MustBeOnline>
<CameraSelection
env={this.props.env}
botOnline={botOnline}
saveFarmwareEnv={this.props.saveFarmwareEnv}
shouldDisplay={this.props.shouldDisplay}
dispatch={this.props.dispatch} />
<BoardType
botOnline={botOnline}
bot={bot}
alerts={this.props.alerts}
dispatch={this.props.dispatch}
shouldDisplay={this.props.shouldDisplay}
timeSettings={this.props.timeSettings}
sourceFbosConfig={sourceFbosConfig} />
<AutoUpdateRow
timeFormat={timeFormat}
device={this.props.deviceAccount}
dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig} />
<FarmbotOsRow
bot={this.props.bot}
osReleaseNotesHeading={this.osReleaseNotes.heading}
osReleaseNotes={this.osReleaseNotes.notes}
dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig}
shouldDisplay={this.props.shouldDisplay}
botOnline={botOnline}
botToMqttLastSeen={new Date(this.props.botToMqttLastSeen).getTime()}
timeSettings={this.props.timeSettings}
deviceAccount={this.props.deviceAccount} />
<AutoSyncRow
dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig} />
{this.props.shouldDisplay(Feature.boot_sequence) &&
<BootSequenceSelector />}
<PowerAndReset
controlPanelState={this.props.bot.controlPanelState}
dispatch={this.props.dispatch}
sourceFbosConfig={sourceFbosConfig}
botOnline={botOnline} />
</WidgetBody>
</form>
</Widget>;

View File

@ -21,7 +21,6 @@ describe("<AutoUpdateRow/>", () => {
const fakeProps = (): AutoUpdateRowProps => ({
timeFormat: "12h",
shouldDisplay: jest.fn(() => true),
device: fakeDevice(),
dispatch: jest.fn(x => x(jest.fn(), () => state)),
sourceFbosConfig: () => ({ value: 1, consistent: true })

View File

@ -69,6 +69,9 @@ describe("<BoardType/>", () => {
{ label: "Arduino/RAMPS (Genesis v1.2)", value: "arduino" },
{ label: "Farmduino (Genesis v1.3)", value: "farmduino" },
{ label: "Farmduino (Genesis v1.4)", value: "farmduino_k14" },
{ label: "Farmduino (Genesis v1.5)", value: "farmduino_k15" },
{ label: "Farmduino (Express v1.0)", value: "express_k10" },
{ label: "None", value: "none" },
]);
});

View File

@ -22,7 +22,6 @@ describe("<FirmwareHardwareStatusDetails />", () => {
apiFirmwareValue: undefined,
botFirmwareValue: undefined,
mcuFirmwareValue: undefined,
shouldDisplay: () => true,
timeSettings: fakeTimeSettings(),
dispatch: jest.fn(),
});
@ -79,7 +78,6 @@ describe("<FirmwareHardwareStatus />", () => {
alerts: [],
botOnline: true,
apiFirmwareValue: undefined,
shouldDisplay: () => true,
timeSettings: fakeTimeSettings(),
dispatch: jest.fn(),
});

View File

@ -7,17 +7,16 @@ import { Content } from "../../../constants";
import { AutoUpdateRowProps } from "./interfaces";
import { t } from "../../../i18next_wrapper";
import { OtaTimeSelector, changeOtaHour } from "./ota_time_selector";
import { Feature } from "../../interfaces";
export function AutoUpdateRow(props: AutoUpdateRowProps) {
const osAutoUpdate = props.sourceFbosConfig("os_auto_update");
return <div>
{props.shouldDisplay(Feature.ota_update_hour) && <OtaTimeSelector
<OtaTimeSelector
timeFormat={props.timeFormat}
disabled={!osAutoUpdate.value}
value={props.device.body.ota_hour}
onChange={changeOtaHour(props.dispatch, props.device)} />}
onChange={changeOtaHour(props.dispatch, props.device)} />
<Row>
<Col xs={ColWidth.label}>
<label>

View File

@ -57,7 +57,7 @@ export class BoardType extends React.Component<BoardTypeProps, BoardTypeState> {
<FBSelect
key={this.apiValue}
extraClass={this.state.sending ? "dim" : ""}
list={getFirmwareChoices(this.props.shouldDisplay)}
list={getFirmwareChoices()}
selectedItem={this.selectedBoard}
onChange={this.sendOffConfig} />
</div>
@ -69,8 +69,7 @@ export class BoardType extends React.Component<BoardTypeProps, BoardTypeState> {
alerts={this.props.alerts}
bot={this.props.bot}
dispatch={this.props.dispatch}
timeSettings={this.props.timeSettings}
shouldDisplay={this.props.shouldDisplay} />
timeSettings={this.props.timeSettings} />
</Col>
</Row>;
}

View File

@ -3,7 +3,7 @@ import { Popover, Position } from "@blueprintjs/core";
import { FIRMWARE_CHOICES_DDI } from "../firmware_hardware_support";
import { flashFirmware } from "../../actions";
import { t } from "../../../i18next_wrapper";
import { BotState, Feature, ShouldDisplay } from "../../interfaces";
import { BotState } from "../../interfaces";
import { FirmwareAlerts } from "../../../messages/alerts";
import { TimeSettings } from "../../../interfaces";
import { trim } from "../../../util";
@ -36,7 +36,6 @@ export interface FirmwareHardwareStatusDetailsProps {
apiFirmwareValue: string | undefined;
botFirmwareValue: string | undefined;
mcuFirmwareValue: string | undefined;
shouldDisplay: ShouldDisplay;
timeSettings: TimeSettings;
dispatch: Function;
}
@ -81,13 +80,10 @@ export const FirmwareHardwareStatusDetails =
<p>{lookup(props.botFirmwareValue) || t("unknown")}</p>
<label>{t("Arduino/Farmduino")}</label>
<p>{lookup(props.mcuFirmwareValue) || t("unknown")}</p>
{props.shouldDisplay(Feature.flash_firmware) &&
<div>
<label>{t("Actions")}</label>
<FirmwareActions
apiFirmwareValue={props.apiFirmwareValue}
botOnline={props.botOnline} />
</div>}
<label>{t("Actions")}</label>
<FirmwareActions
apiFirmwareValue={props.apiFirmwareValue}
botOnline={props.botOnline} />
<FirmwareAlerts
alerts={props.alerts}
dispatch={props.dispatch}
@ -102,7 +98,6 @@ export interface FirmwareHardwareStatusProps {
bot: BotState;
botOnline: boolean;
timeSettings: TimeSettings;
shouldDisplay: ShouldDisplay;
dispatch: Function;
}
@ -122,7 +117,6 @@ export const FirmwareHardwareStatus = (props: FirmwareHardwareStatusProps) => {
botFirmwareValue={firmware_hardware}
mcuFirmwareValue={boardType(firmware_version)}
timeSettings={props.timeSettings}
dispatch={props.dispatch}
shouldDisplay={props.shouldDisplay} />
dispatch={props.dispatch} />
</Popover>;
};

View File

@ -24,7 +24,6 @@ export interface AutoUpdateRowProps {
timeFormat: PreferredHourFormat;
sourceFbosConfig: SourceFbosConfig;
device: TaggedDevice;
shouldDisplay: ShouldDisplay;
}
export interface CameraSelectionProps {

View File

@ -1,5 +1,4 @@
import { FirmwareHardware } from "farmbot";
import { ShouldDisplay, Feature } from "../interfaces";
export const isFwHardwareValue = (x?: unknown): x is FirmwareHardware => {
const values: FirmwareHardware[] = [
@ -77,12 +76,11 @@ export const FIRMWARE_CHOICES_DDI = {
[NONE.value]: NONE
};
export const getFirmwareChoices =
(shouldDisplay: ShouldDisplay = () => true) => ([
ARDUINO,
FARMDUINO,
FARMDUINO_K14,
...(shouldDisplay(Feature.farmduino_k15) ? [FARMDUINO_K15] : []),
...(shouldDisplay(Feature.express_k10) ? [EXPRESS_K10] : []),
...(shouldDisplay(Feature.none_firmware) ? [NONE] : []),
]);
export const getFirmwareChoices = () => ([
ARDUINO,
FARMDUINO,
FARMDUINO_K14,
FARMDUINO_K15,
EXPRESS_K10,
NONE,
]);

View File

@ -2,7 +2,7 @@ import * as React from "react";
import { MCUFactoryReset, bulkToggleControlPanel } from "../actions";
import { Widget, WidgetHeader, WidgetBody } from "../../ui/index";
import { HardwareSettingsProps } from "../interfaces";
import { MustBeOnline, isBotOnline } from "../must_be_online";
import { isBotOnline } from "../must_be_online";
import { ToolTips } from "../../constants";
import { DangerZone } from "./hardware_settings/danger_zone";
import { PinGuard } from "./hardware_settings/pin_guard";
@ -32,14 +32,7 @@ export class HardwareSettings extends
const botDisconnected = !isBotOnline(sync_status, botToMqttStatus);
const commonProps = { dispatch, controlPanelState };
return <Widget className="hardware-widget">
<WidgetHeader title={t("Hardware")} helpText={ToolTips.HW_SETTINGS}>
<MustBeOnline
hideBanner={true}
syncStatus={sync_status}
networkState={this.props.botToMqttStatus}
lockOpen={process.env.NODE_ENV !== "production"}>
</MustBeOnline>
</WidgetHeader>
<WidgetHeader title={t("Hardware")} helpText={ToolTips.HW_SETTINGS} />
<WidgetBody>
<button
className={"fb-button gray no-float"}
@ -51,43 +44,37 @@ export class HardwareSettings extends
onClick={() => dispatch(bulkToggleControlPanel(false))}>
{t("Collapse All")}
</button>
{firmwareConfig &&
<Popover position={Position.BOTTOM_RIGHT}>
<i className="fa fa-download" />
<FwParamExportMenu firmwareConfig={firmwareConfig} />
</Popover>}
<MustBeOnline
networkState={this.props.botToMqttStatus}
syncStatus={sync_status}
lockOpen={process.env.NODE_ENV !== "production" || !!firmwareConfig}>
<div className="label-headings">
<SpacePanelHeader />
</div>
<HomingAndCalibration {...commonProps}
bot={bot}
sourceFwConfig={sourceFwConfig}
firmwareConfig={firmwareConfig}
firmwareHardware={firmwareHardware}
botDisconnected={botDisconnected} />
<Motors {...commonProps}
sourceFwConfig={sourceFwConfig}
firmwareHardware={firmwareHardware} />
<Encoders {...commonProps}
sourceFwConfig={sourceFwConfig}
firmwareHardware={firmwareHardware} />
<EndStops {...commonProps}
sourceFwConfig={sourceFwConfig} />
<ErrorHandling {...commonProps}
sourceFwConfig={sourceFwConfig} />
<PinGuard {...commonProps}
resources={resources}
sourceFwConfig={sourceFwConfig} />
<DangerZone {...commonProps}
onReset={MCUFactoryReset}
botDisconnected={botDisconnected} />
<PinBindings {...commonProps}
resources={resources} />
</MustBeOnline>
<Popover position={Position.BOTTOM_RIGHT}>
<i className="fa fa-download" />
<FwParamExportMenu firmwareConfig={firmwareConfig} />
</Popover>
<div className="label-headings">
<SpacePanelHeader />
</div>
<HomingAndCalibration {...commonProps}
bot={bot}
sourceFwConfig={sourceFwConfig}
firmwareConfig={firmwareConfig}
firmwareHardware={firmwareHardware}
botDisconnected={botDisconnected} />
<Motors {...commonProps}
sourceFwConfig={sourceFwConfig}
firmwareHardware={firmwareHardware} />
<Encoders {...commonProps}
sourceFwConfig={sourceFwConfig}
firmwareHardware={firmwareHardware} />
<EndStops {...commonProps}
sourceFwConfig={sourceFwConfig} />
<ErrorHandling {...commonProps}
sourceFwConfig={sourceFwConfig} />
<PinGuard {...commonProps}
resources={resources}
sourceFwConfig={sourceFwConfig} />
<DangerZone {...commonProps}
onReset={MCUFactoryReset}
botDisconnected={botDisconnected} />
<PinBindings {...commonProps}
resources={resources} />
</WidgetBody>
</Widget>;
}

View File

@ -25,7 +25,7 @@ const getSubKeyName = (key: string) => {
};
export const FwParamExportMenu =
({ firmwareConfig }: { firmwareConfig: FirmwareConfig }) => {
({ firmwareConfig }: { firmwareConfig: FirmwareConfig | undefined }) => {
/** Filter out unnecessary parameters. */
const filteredConfig = pickBy(firmwareConfig, (_, key) =>
!["id", "device_id", "api_migrated", "created_at", "updated_at",

View File

@ -11,7 +11,6 @@ import { maybeNegateStatus } from "../connectivity/maybe_negate_status";
import { ReduxAction } from "../redux/interfaces";
import { connectivityReducer, PingResultPayload } from "../connectivity/reducer";
import { versionOK } from "../util";
import { EXPECTED_MAJOR, EXPECTED_MINOR } from "./actions";
import { DeepPartial } from "redux";
import { incomingLegacyStatus } from "../connectivity/connect_device";
import { merge } from "lodash";
@ -205,8 +204,7 @@ function legacyStatusHandler(state: BotState,
const nextSyncStatus = maybeNegateStatus(info);
versionOK(informational_settings.controller_version,
EXPECTED_MAJOR, EXPECTED_MINOR);
versionOK(informational_settings.controller_version);
state.hardware.informational_settings.sync_status = nextSyncStatus;
return state;
}

View File

@ -8,7 +8,7 @@ export function FarmBotLayer(props: FarmBotLayerProps) {
visible, stopAtHome, botSize, plantAreaOffset, mapTransformProps,
peripherals, eStopStatus, botLocationData, getConfigValue
} = props;
return visible ? <g id="farmbot-layer">
return visible ? <g id="farmbot-layer" style={{ pointerEvents: "none" }}>
<VirtualFarmBot
mapTransformProps={mapTransformProps}
botLocationData={botLocationData}

View File

@ -50,6 +50,14 @@ describe("<ToolbaySlot />", () => {
const wrapper = svgMount(<ToolbaySlot {...p} />);
expect(wrapper.find("use").props().transform).toEqual(expected);
});
it("handles bad data", () => {
const p = fakeProps();
p.pulloutDirection = 1.1;
p.quadrant = 1.1;
const wrapper = svgMount(<ToolbaySlot {...p} />);
expect(wrapper.find("use").props().transform).toEqual("rotate(0, 10, 20)");
});
});
describe("<Tool/>", () => {

View File

@ -49,4 +49,8 @@ describe("textAnchorPosition()", () => {
expect(textAnchorPosition(4, 3, true)).toEqual(END);
expect(textAnchorPosition(4, 4, true)).toEqual(START);
});
it("handles bad data", () => {
expect(textAnchorPosition(1.1, 1.1, false)).toEqual(START);
});
});

View File

@ -4,6 +4,11 @@ jest.mock("../../../../../history", () => ({
getPathArray: jest.fn(() => { return mockPath.split("/"); })
}));
let mockDev = false;
jest.mock("../../../../../account/dev/dev_support", () => ({
DevSettings: { futureFeaturesEnabled: () => mockDev }
}));
import * as React from "react";
import { ToolSlotLayer, ToolSlotLayerProps } from "../tool_slot_layer";
import {
@ -53,6 +58,7 @@ describe("<ToolSlotLayer/>", () => {
});
it("navigates to tools page", async () => {
mockDev = true;
mockPath = "/app/designer/plants";
const p = fakeProps();
const wrapper = shallow(<ToolSlotLayer {...p} />);

View File

@ -48,10 +48,10 @@ describe("<ToolSlotPoint/>", () => {
const p = fakeProps();
p.slot.toolSlot.body.id = 1;
const wrapper = svgMount(<ToolSlotPoint {...p} />);
mockDev = false;
mockDev = true;
wrapper.find("g").first().simulate("click");
expect(history.push).not.toHaveBeenCalled();
mockDev = true;
mockDev = false;
wrapper.find("g").first().simulate("click");
expect(history.push).toHaveBeenCalledWith("/app/designer/tool-slots/1");
});

View File

@ -180,23 +180,30 @@ export interface GantryToolSlotGraphicProps {
xySwap: boolean;
}
/** dimensions */
enum Trough {
width = 20,
length = 45,
wall = 4,
}
export const GantryToolSlot = (props: GantryToolSlotGraphicProps) => {
const { x, y, xySwap } = props;
const slotLengthX = xySwap ? 24 : 49;
const slotLengthY = xySwap ? 49 : 24;
const slotLengthX = Trough.wall + (xySwap ? Trough.width : Trough.length);
const slotLengthY = Trough.wall + (xySwap ? Trough.length : Trough.width);
return <g id={"gantry-toolbay-slot"}>
<rect
x={x - slotLengthX / 2} y={y - slotLengthY / 2}
width={slotLengthX} height={slotLengthY}
stroke={Color.mediumGray} strokeWidth={4} strokeOpacity={0.25}
stroke={Color.mediumGray} strokeWidth={Trough.wall} strokeOpacity={0.25}
fill="transparent" />
</g>;
};
const SeedTrough = (props: ToolGraphicProps) => {
const { x, y, hovered, dispatch, uuid, xySwap } = props;
const slotLengthX = xySwap ? 20 : 45;
const slotLengthY = xySwap ? 45 : 20;
const slotLengthX = xySwap ? Trough.width : Trough.length;
const slotLengthY = xySwap ? Trough.length : Trough.width;
return <g id={"seed-trough"}
onMouseOver={() => dispatch(setToolHover(uuid))}
onMouseLeave={() => dispatch(setToolHover(undefined))}>

View File

@ -39,7 +39,7 @@ export const textAnchorPosition = (
case Anchor.end: return { anchor: "end", x: -40, y: 10 };
case Anchor.middleTop: return { anchor: "middle", x: 0, y: 60 };
case Anchor.middleBottom: return { anchor: "middle", x: 0, y: -40 };
default: return { anchor: "start", x: 40, y: 10 };
default: throw new Error("https://xkcd.com/2200");
}
};

View File

@ -19,7 +19,7 @@ export function ToolSlotLayer(props: ToolSlotLayerProps) {
const pathArray = getPathArray();
const canClickTool = !(pathArray[3] === "plants" && pathArray.length > 4);
const goToToolsPage = () => canClickTool &&
!DevSettings.futureFeaturesEnabled() && history.push("/app/tools");
DevSettings.futureFeaturesEnabled() && history.push("/app/tools");
const { slots, visible, mapTransformProps } = props;
const cursor = canClickTool ? "pointer" : "default";

View File

@ -43,7 +43,7 @@ export const ToolSlotPoint = (props: TSPProps) => {
xySwap,
};
return <g id={"toolslot-" + id}
onClick={() => DevSettings.futureFeaturesEnabled() &&
onClick={() => !DevSettings.futureFeaturesEnabled() &&
history.push(`/app/designer/tool-slots/${id}`)}>
{pullout_direction &&
<ToolbaySlot

View File

@ -139,7 +139,7 @@ export function DesignerNavTabs(props: { hidden?: boolean }) {
panel={Panel.Weeds}
linkTo={"/app/designer/weeds"}
title={t("Weeds")} />
{DevSettings.futureFeaturesEnabled() &&
{!DevSettings.futureFeaturesEnabled() &&
<NavTab
panel={Panel.Tools}
linkTo={"/app/designer/tools"}

View File

@ -7,7 +7,7 @@ import * as React from "react";
import { mount } from "enzyme";
import { PlantGrid } from "../plant_grid";
import { saveGrid, stashGrid } from "../thunks";
import { error } from "../../../../toast/toast";
import { error, success } from "../../../../toast/toast";
describe("PlantGrid", () => {
function fakeProps() {
@ -39,8 +39,11 @@ describe("PlantGrid", () => {
it("saves a grid", async () => {
const props = fakeProps();
const pg = mount<PlantGrid>(<PlantGrid {...props} />).instance();
const oldId = pg.state.gridId;
await pg.saveGrid();
expect(saveGrid).toHaveBeenCalledWith(pg.state.gridId);
expect(saveGrid).toHaveBeenCalledWith(oldId);
expect(success).toHaveBeenCalledWith("16 plants added.");
expect(pg.state.gridId).not.toEqual(oldId);
});
it("stashes a grid", async () => {

View File

@ -9,13 +9,18 @@ import { initPlantGrid } from "./generate_grid";
import { init } from "../../../api/crud";
import { uuid } from "farmbot";
import { saveGrid, stashGrid } from "./thunks";
import { error } from "../../../toast/toast";
import { error, success } from "../../../toast/toast";
import { t } from "../../../i18next_wrapper";
import { GridInput } from "./grid_input";
export class PlantGrid extends React.Component<PlantGridProps, PlantGridState> {
state: PlantGridState = { ...EMPTY_PLANT_GRID, gridId: uuid() };
get plantCount() {
const { numPlantsH, numPlantsV } = this.state.grid;
return numPlantsH * numPlantsV;
}
onchange = (key: PlantGridKey, val: number) => {
const grid = { ...this.state.grid, [key]: val };
this.setState({ grid });
@ -33,9 +38,7 @@ export class PlantGrid extends React.Component<PlantGridProps, PlantGridState> {
}
performPreview = () => {
const { numPlantsH, numPlantsV } = this.state.grid;
const total = numPlantsH * numPlantsV;
if (total > 100) {
if (this.plantCount > 100) {
error(t("Please make a grid with less than 100 plants"));
return;
}
@ -57,7 +60,10 @@ export class PlantGrid extends React.Component<PlantGridProps, PlantGridState> {
saveGrid = () => {
const p: Promise<{}> = this.props.dispatch(saveGrid(this.state.gridId));
return p.then(() => this.setState(EMPTY_PLANT_GRID));
return p.then(() => {
success(t("{{ count }} plants added.", { count: this.plantCount }));
this.setState({ ...EMPTY_PLANT_GRID, gridId: uuid() });
});
}
inputs = () => {
@ -73,16 +79,16 @@ export class PlantGrid extends React.Component<PlantGridProps, PlantGridState> {
case "clean":
return <div>
<a className={"clear-button"} onClick={this.performPreview}>
Preview
{t("Preview")}
</a>
</div>;
case "dirty":
return <div>
<a className={"clear-button"} onClick={this.revertPreview}>
Cancel
{t("Cancel")}
</a>
<a className={"clear-button"} onClick={this.saveGrid}>
Save
{t("Save")}
</a>
</div>;
}

View File

@ -24,6 +24,7 @@ import {
import { save, edit } from "../../../api/crud";
import { SpecialStatus } from "farmbot";
import { DEFAULT_CRITERIA } from "../criteria/interfaces";
import { Content } from "../../../constants";
describe("<GroupDetailActive/>", () => {
const fakeProps = (): GroupDetailActiveProps => {
@ -105,16 +106,23 @@ describe("<GroupDetailActive/>", () => {
});
it("shows paths", () => {
mockDev = true;
const p = fakeProps();
const wrapper = mount(<GroupDetailActive {...p} />);
expect(wrapper.text().toLowerCase()).toContain("optimized");
});
it("doesn't show paths", () => {
mockDev = false;
const p = fakeProps();
const wrapper = mount(<GroupDetailActive {...p} />);
expect(wrapper.text().toLowerCase()).not.toContain("optimized");
expect(wrapper.text().toLowerCase()).toContain("0m");
});
it("doesn't show paths", () => {
mockDev = true;
const p = fakeProps();
const wrapper = mount(<GroupDetailActive {...p} />);
expect(wrapper.text().toLowerCase()).not.toContain("0m");
});
it("shows random warning text", () => {
const p = fakeProps();
p.group.body.sort_type = "random";
const wrapper = mount(<GroupDetailActive {...p} />);
expect(wrapper.text()).toContain(Content.SORT_DESCRIPTION);
});
});

View File

@ -1,5 +1,12 @@
jest.mock("../../../api/crud", () => ({ edit: jest.fn() }));
let mockDev = false;
jest.mock("../../../account/dev/dev_support", () => ({
DevSettings: {
futureFeaturesEnabled: () => mockDev,
}
}));
import * as React from "react";
import { shallow, mount } from "enzyme";
import {
@ -141,6 +148,7 @@ describe("<Paths />", () => {
p.pathPoints = cases.order.xy_ascending;
const wrapper = mount<Paths>(<Paths {...p} />);
expect(wrapper.state().pathData).toEqual(cases.distance);
expect(wrapper.text().toLowerCase()).not.toContain("optimized");
});
it.each<[PointGroupSortType]>([
@ -154,4 +162,24 @@ describe("<Paths />", () => {
expect(SORT_OPTIONS[sortType](cases.order.xy_ascending))
.toEqual(cases.order[sortType]);
});
it("renders new sort type", () => {
mockDev = true;
const p = fakeProps();
const cases = pathTestCases();
p.pathPoints = cases.order.xy_ascending;
const wrapper = mount<Paths>(<Paths {...p} />);
expect(wrapper.text().toLowerCase()).toContain("optimized");
});
it("doesn't generate data twice", () => {
const p = fakeProps();
const cases = pathTestCases();
p.pathPoints = cases.order.xy_ascending;
const wrapper = mount<Paths>(<Paths {...p} />);
expect(wrapper.state().pathData).toEqual(cases.distance);
wrapper.setState({ pathData: { nn: 0 } });
wrapper.update();
expect(wrapper.state().pathData).toEqual({ nn: 0 });
});
});

View File

@ -45,7 +45,6 @@ describe("<PointGroupItem/>", () => {
const p = fakeProps();
p.point = fakePlant();
const i = new PointGroupItem(p);
i.setState = jest.fn();
const fakeImgEvent = imgEvent();
await i.maybeGetCachedIcon(fakeImgEvent);
const slug = i.props.point.body.pointer_type === "Plant" ?
@ -55,11 +54,17 @@ describe("<PointGroupItem/>", () => {
expect(setImgSrc).not.toHaveBeenCalled();
});
it("sets icon in state", () => {
const i = new PointGroupItem(fakeProps());
i.setState = jest.fn();
i.setIconState("fake icon");
expect(i.setState).toHaveBeenCalledWith({ icon: "fake icon" });
});
it("fetches point icon", () => {
const p = fakeProps();
p.point = fakePoint();
const i = new PointGroupItem(p);
i.setState = jest.fn();
const fakeImgEvent = imgEvent();
i.maybeGetCachedIcon(fakeImgEvent);
expect(maybeGetCachedPlantIcon).not.toHaveBeenCalled();
@ -71,7 +76,6 @@ describe("<PointGroupItem/>", () => {
const p = fakeProps();
p.point = fakeToolSlot();
const i = new PointGroupItem(p);
i.setState = jest.fn();
const fakeImgEvent = imgEvent();
i.maybeGetCachedIcon(fakeImgEvent);
expect(maybeGetCachedPlantIcon).not.toHaveBeenCalled();

View File

@ -1,14 +1,10 @@
import * as React from "react";
import {
isSortType, sortTypeChange, SORT_OPTIONS, PointGroupSortSelector,
PointGroupSortSelectorProps
isSortType, sortTypeChange, SORT_OPTIONS
} from "../point_group_sort_selector";
import { DropDownItem } from "../../../ui";
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
import { TaggedPoint } from "farmbot";
import { fakePlant } from "../../../__test_support__/fake_state/resources";
import { mount } from "enzyme";
import { Content } from "../../../constants";
const tests: [string, boolean][] = [
["", false],
@ -89,15 +85,3 @@ describe("sort()", () => {
expect(results).toEqual(["C", "D", "B", "A"]);
});
});
describe("<PointGroupSortSelector />", () => {
const fakeProps = (): PointGroupSortSelectorProps => ({
onChange: jest.fn(),
value: "random",
});
it("shows random warning text", () => {
const wrapper = mount(<PointGroupSortSelector {...fakeProps()} />);
expect(wrapper.text()).toContain(Content.SORT_DESCRIPTION);
});
});

View File

@ -97,9 +97,24 @@ export class GroupDetailActive
defaultValue={group.body.name}
onChange={this.update}
onBlur={this.saveGroup} />
<PointGroupSortSelector
value={group.body.sort_type}
onChange={this.changeSortType} />
<div>
<label>
{t("SORT BY")}
</label>
{!DevSettings.futureFeaturesEnabled()
? <Paths
key={JSON.stringify(this.pointsSelectedByGroup
.map(p => p.body.id))}
pathPoints={this.pointsSelectedByGroup}
dispatch={dispatch}
group={group} />
: <PointGroupSortSelector
value={group.body.sort_type}
onChange={this.changeSortType} />}
<p>
{group.body.sort_type == "random" && t(Content.SORT_DESCRIPTION)}
</p>
</div>
<label>
{t("GROUP MEMBERS ({{count}})", { count: this.icons.length })}
</label>
@ -117,11 +132,6 @@ export class GroupDetailActive
{this.props.shouldDisplay(Feature.criteria_groups) &&
<GroupCriteria dispatch={dispatch}
group={group} slugs={this.props.slugs} />}
{DevSettings.futureFeaturesEnabled() &&
<Paths
pathPoints={this.pointsSelectedByGroup}
dispatch={dispatch}
group={group} />}
<DeleteButton
className="group-delete-btn"
dispatch={dispatch}

View File

@ -1,7 +1,7 @@
import * as React from "react";
import { MapTransformProps } from "../map/interfaces";
import { sortGroupBy, sortOptionsTable } from "./point_group_sort_selector";
import { sortBy } from "lodash";
import { sortBy, isNumber } from "lodash";
import { PointsPathLine } from "./group_order_visual";
import { Color } from "../../ui";
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
@ -10,6 +10,7 @@ import { Actions } from "../../constants";
import { edit } from "../../api/crud";
import { TaggedPointGroup, TaggedPoint } from "farmbot";
import { error } from "../../toast/toast";
import { DevSettings } from "../../account/dev/dev_support";
const xy = (point: TaggedPoint) => ({ x: point.body.x, y: point.body.y });
@ -66,7 +67,8 @@ export const PathInfoBar = (props: PathInfoBarProps) => {
const normalizedLength = pathLength / maxLength * 100;
const sortLabel =
sortTypeKey == "nn" ? "Optimized" : sortOptionsTable()[sortTypeKey];
return <div className={"sort-path-info-bar"}
const selected = group.body.sort_type == sortTypeKey;
return <div className={`sort-option-bar ${selected ? "selected" : ""}`}
onMouseEnter={() =>
dispatch({ type: Actions.TRY_SORT_TYPE, payload: sortTypeKey })}
onMouseLeave={() =>
@ -74,9 +76,11 @@ export const PathInfoBar = (props: PathInfoBarProps) => {
onClick={() =>
sortTypeKey == "nn"
? error(t("Not supported yet."))
: dispatch(edit(group, { sort_type: sortTypeKey }))}
style={{ width: `${normalizedLength}%` }}>
{`${sortLabel}: ${Math.round(pathLength / 10) / 100}m`}
: dispatch(edit(group, { sort_type: sortTypeKey }))}>
<div className={"sort-path-info-bar"}
style={{ width: `${normalizedLength}%` }}>
{`${sortLabel}: ${Math.round(pathLength / 10) / 100}m`}
</div>
</div>;
};
@ -101,15 +105,17 @@ export class Paths extends React.Component<PathsProps, PathsState> {
};
render() {
if (!this.state.pathData.nn) { this.generatePathData(this.props.pathPoints); }
return <div>
<label>{t("Path lengths by sort type")}</label>
{SORT_TYPES.concat("nn").map(st =>
<PathInfoBar key={st}
sortTypeKey={st}
dispatch={this.props.dispatch}
group={this.props.group}
pathData={this.state.pathData} />)}
if (!isNumber(this.state.pathData.nn)) {
this.generatePathData(this.props.pathPoints);
}
return <div className={"group-sort-types"}>
{SORT_TYPES.concat(DevSettings.futureFeaturesEnabled() ? "nn" : [])
.map(sortType =>
<PathInfoBar key={sortType}
sortTypeKey={sortType}
dispatch={this.props.dispatch}
group={this.props.group}
pathData={this.state.pathData} />)}
</div>;
}
}

View File

@ -57,6 +57,8 @@ export class PointGroupItem
this.leave();
}
setIconState = (icon: string) => this.setState({ icon });
get criteriaIcon() {
return !this.props.group.body.point_ids
.includes(this.props.point.body.id || 0);
@ -67,7 +69,7 @@ export class PointGroupItem
switch (this.props.point.body.pointer_type) {
case "Plant":
const slug = this.props.point.body.openfarm_slug;
maybeGetCachedPlantIcon(slug, img, icon => this.setState({ icon }));
maybeGetCachedPlantIcon(slug, img, this.setIconState);
break;
case "GenericPointer":
const { color } = this.props.point.body.meta;

View File

@ -3,7 +3,6 @@ import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
import { FBSelect, DropDownItem } from "../../ui";
import { t } from "../../i18next_wrapper";
import { shuffle, sortBy } from "lodash";
import { Content } from "../../constants";
import { TaggedPoint } from "farmbot";
export interface PointGroupSortSelectorProps {
@ -42,22 +41,11 @@ export const sortTypeChange = (cb: Function) => (ddi: DropDownItem) => {
};
export function PointGroupSortSelector(p: PointGroupSortSelectorProps) {
return <div>
<div className="default-value-tooltip">
<label>
{t("SORT BY")}
</label>
</div>
<FBSelect
key={p.value}
list={optionPlusDescriptions()}
selectedItem={selected(p.value as PointGroupSortType)}
onChange={sortTypeChange(p.onChange)} />
<p>
{(p.value == "random") ? t(Content.SORT_DESCRIPTION) : ""}
</p>
</div>;
return <FBSelect
key={p.value}
list={optionPlusDescriptions()}
selectedItem={selected(p.value as PointGroupSortType)}
onChange={sortTypeChange(p.onChange)} />;
}
type Sorter = (p: TaggedPoint[]) => TaggedPoint[];

View File

@ -11,6 +11,7 @@ import { fakeState } from "../../../__test_support__/fake_state";
import { SaveBtn } from "../../../ui";
import { initSave } from "../../../api/crud";
import { history } from "../../../history";
import { error } from "../../../toast/toast";
describe("<AddTool />", () => {
const fakeProps = (): AddToolProps => ({
@ -37,10 +38,19 @@ describe("<AddTool />", () => {
expect(initSave).toHaveBeenCalledWith("Tool", { name: "Foo" });
});
it("adds stock tools", () => {
it("doesn't add stock tools", () => {
const wrapper = mount(<AddTool {...fakeProps()} />);
wrapper.find("button").last().simulate("click");
expect(initSave).toHaveBeenCalledTimes(6);
expect(error).toHaveBeenCalledWith("Please choose a FarmBot model.");
expect(initSave).not.toHaveBeenCalledTimes(6);
expect(history.push).not.toHaveBeenCalledWith("/app/designer/tools");
});
it("adds stock tools", () => {
const wrapper = mount(<AddTool {...fakeProps()} />);
wrapper.setState({ model: "express" });
wrapper.find("button").last().simulate("click");
expect(initSave).toHaveBeenCalledTimes(2);
expect(history.push).toHaveBeenCalledWith("/app/designer/tools");
});
});

View File

@ -5,11 +5,20 @@ import {
} from "../designer_panel";
import { Everything } from "../../interfaces";
import { t } from "../../i18next_wrapper";
import { SaveBtn } from "../../ui";
import { SaveBtn, FBSelect, DropDownItem } from "../../ui";
import { SpecialStatus } from "farmbot";
import { initSave } from "../../api/crud";
import { Panel } from "../panel_header";
import { history } from "../../history";
import { error } from "../../toast/toast";
enum Model { genesis14 = "genesis14", genesis15 = "genesis15", express = "express" }
const MODEL_DDI_LOOKUP = (): { [x: string]: DropDownItem } => ({
[Model.genesis14]: { label: t("Genesis v1.2-v1.4"), value: Model.genesis14 },
[Model.genesis15]: { label: t("Genesis v1.5+"), value: Model.genesis15 },
[Model.express]: { label: t("Express"), value: Model.express },
});
export interface AddToolProps {
dispatch: Function;
@ -17,6 +26,7 @@ export interface AddToolProps {
export interface AddToolState {
toolName: string;
model: Model | undefined;
}
export const mapStateToProps = (props: Everything): AddToolProps => ({
@ -24,7 +34,7 @@ export const mapStateToProps = (props: Everything): AddToolProps => ({
});
export class RawAddTool extends React.Component<AddToolProps, AddToolState> {
state: AddToolState = { toolName: "" };
state: AddToolState = { toolName: "", model: undefined };
newTool = (name: string) => {
this.props.dispatch(initSave("Tool", { name }));
@ -35,28 +45,60 @@ export class RawAddTool extends React.Component<AddToolProps, AddToolState> {
history.push("/app/designer/tools");
}
get stockToolNames() {
return [
t("Seeder"),
t("Watering Nozzle"),
t("Weeder"),
t("Soil Sensor"),
t("Seed Bin"),
t("Seed Tray"),
];
stockToolNames = (model: Model) => {
switch (model) {
case Model.genesis14:
return [
t("Seeder"),
t("Watering Nozzle"),
t("Weeder"),
t("Soil Sensor"),
t("Seed Bin"),
t("Seed Tray"),
];
case Model.genesis15:
return [
t("Seeder"),
t("Watering Nozzle"),
t("Weeder"),
t("Soil Sensor"),
t("Seed Bin"),
t("Seed Tray"),
t("Seed Trough 1"),
t("Seed Trough 2"),
];
case Model.express:
return [
t("Seed Trough 1"),
t("Seed Trough 2"),
];
}
}
AddStockTools = () =>
<div className="add-stock-tools">
<label>{t("Add stock tools")}</label>
<ul>
{this.stockToolNames.map(n => <li key={n}>{n}</li>)}
</ul>
<FBSelect
customNullLabel={t("Choose model")}
list={Object.values(MODEL_DDI_LOOKUP())}
selectedItem={this.state.model
? MODEL_DDI_LOOKUP()[this.state.model]
: undefined}
onChange={ddi => this.setState({ model: ddi.value as Model })}
/>
{this.state.model &&
<ul>
{this.stockToolNames(this.state.model).map(n => <li key={n}>{n}</li>)}
</ul>}
<button
className="fb-button green"
onClick={() => {
this.stockToolNames.map(n => this.newTool(n));
history.push("/app/designer/tools");
if (this.state.model) {
this.stockToolNames(this.state.model).map(n => this.newTool(n));
history.push("/app/designer/tools");
} else {
error(t("Please choose a FarmBot model."));
}
}}>
<i className="fa fa-plus" />
{t("Stock Tools")}

View File

@ -159,14 +159,20 @@ export class RawTools extends React.Component<ToolsProps, ToolsState> {
getToolName={this.getToolName} />)}
</div>
InactiveTools = () =>
<div className="inactive-tools">
<label>{t("inactive tools")}</label>
Tools = () =>
<div className="tools">
<div className="tools-header">
<label>{t("tools")}</label>
<Link to={"/app/designer/tools/add"}>
<div className={`fb-button panel-${TAB_COLOR[Panel.Tools]}`}>
<i className="fa fa-plus" title={t("Add tool")} />
</div>
</Link>
</div>
{this.props.tools
.filter(tool => !tool.body.name ||
tool.body.name && tool.body.name.toLowerCase()
.includes(this.state.searchTerm.toLowerCase()))
.filter(tool => tool.body.status === "inactive")
.map(tool =>
<ToolInventoryItem key={tool.uuid}
toolId={tool.body.id}
@ -175,25 +181,26 @@ export class RawTools extends React.Component<ToolsProps, ToolsState> {
render() {
const panelName = "tools";
const hasTools = this.props.tools.length > 0;
return <DesignerPanel panelName={panelName} panel={Panel.Tools}>
<DesignerNavTabs />
<DesignerPanelTop
panel={Panel.Tools}
linkTo={"/app/designer/tools/add"}
title={t("Add tool")}>
linkTo={!hasTools ? "/app/designer/tools/add" : undefined}
title={!hasTools ? t("Add tool") : undefined}>
<input type="text" onChange={this.update}
placeholder={t("Search your tools...")} />
</DesignerPanelTop>
<DesignerPanelContent panelName={"tools"}>
<EmptyStateWrapper
notEmpty={this.props.tools.length > 0}
notEmpty={hasTools}
graphic={EmptyStateGraphic.tools}
title={t("Add a tool")}
text={Content.NO_TOOLS}
colorScheme={"tools"}>
<this.MountedToolInfo />
<this.ToolSlots />
<this.InactiveTools />
<this.Tools />
</EmptyStateWrapper>
</DesignerPanelContent>
</DesignerPanel>;

View File

@ -3,6 +3,13 @@ jest.mock("../../../device", () => ({ getDevice: () => mockDevice }));
jest.mock("../actions", () => ({ scanImage: jest.fn() }));
jest.mock("../../images/actions", () => ({ selectImage: jest.fn() }));
let mockDev = false;
jest.mock("../../../account/dev/dev_support", () => ({
DevSettings: {
futureFeaturesEnabled: () => mockDev,
}
}));
import * as React from "react";
import { mount, shallow } from "enzyme";
import { CameraCalibration } from "../camera_calibration";
@ -12,6 +19,7 @@ import { selectImage } from "../../images/actions";
import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings";
import { error } from "../../../toast/toast";
import { Content, ToolTips } from "../../../constants";
import { SPECIAL_VALUES } from "../../weed_detector/remote_env/constants";
describe("<CameraCalibration/>", () => {
const fakeProps = (): CameraCalibrationProps => ({
@ -116,4 +124,21 @@ describe("<CameraCalibration/>", () => {
expect(error).toHaveBeenCalledWith(
ToolTips.SELECT_A_CAMERA, Content.NO_CAMERA_SELECTED);
});
it("toggles simple version", () => {
mockDev = true;
const p = fakeProps();
const wrapper = mount(<CameraCalibration {...p} />);
wrapper.find("input").first().simulate("change");
expect(mockDevice.setUserEnv).toHaveBeenCalledWith({
CAMERA_CALIBRATION_easy_calibration: "\"FALSE\""
});
});
it("renders simple version", () => {
const p = fakeProps();
p.wDEnv = { CAMERA_CALIBRATION_easy_calibration: SPECIAL_VALUES.TRUE };
const wrapper = mount(<CameraCalibration {...p} />);
expect(wrapper.text().toLowerCase()).not.toContain("blur");
});
});

View File

@ -8,7 +8,7 @@ import { selectImage } from "../images/actions";
import { calibrate, scanImage } from "./actions";
import { envGet } from "../weed_detector/remote_env/selectors";
import { MustBeOnline, isBotOnline } from "../../devices/must_be_online";
import { WeedDetectorConfig } from "../weed_detector/config";
import { WeedDetectorConfig, BoolConfig } from "../weed_detector/config";
import { Feature } from "../../devices/interfaces";
import { namespace } from "../weed_detector";
import { t } from "../../i18next_wrapper";
@ -16,6 +16,10 @@ import { formatEnvKey } from "../weed_detector/remote_env/translators";
import {
cameraBtnProps
} from "../../devices/components/fbos_settings/camera_selection";
import { ImageFlipper } from "../images/image_flipper";
import { PhotoFooter } from "../images/photos";
import { UUID } from "../../resources/interfaces";
import { DevSettings } from "../../account/dev/dev_support";
export class CameraCalibration extends
React.Component<CameraCalibrationProps, {}> {
@ -31,9 +35,11 @@ export class CameraCalibration extends
key, JSON.stringify(formatEnvKey(key, value))))
: envSave(key, value)
onFlip = (uuid: UUID) => this.props.dispatch(selectImage(uuid));
render() {
const camDisabled = cameraBtnProps(this.props.env);
return <div className="weed-detector">
return <div className="camera-calibration">
<div className="farmware-button">
<MustBeOnline
syncStatus={this.props.syncStatus}
@ -50,15 +56,27 @@ export class CameraCalibration extends
</div>
<Row>
<Col sm={12}>
<MustBeOnline
syncStatus={this.props.syncStatus}
networkState={this.props.botToMqttStatus}
lockOpen={process.env.NODE_ENV !== "production"}>
<ImageWorkspace
{DevSettings.futureFeaturesEnabled() &&
<BoolConfig
wDEnv={this.props.wDEnv}
configKey={this.namespace("easy_calibration")}
label={t("Simpler")}
onChange={this.saveEnvVar} />}
{!!envGet(this.namespace("easy_calibration"), this.props.wDEnv)
? <div className={"flipper-section"}>
<ImageFlipper
onFlip={this.onFlip}
images={this.props.images}
currentImage={this.props.currentImage} />
<PhotoFooter
image={this.props.currentImage}
timeSettings={this.props.timeSettings} />
</div>
: <ImageWorkspace
botOnline={
isBotOnline(this.props.syncStatus, this.props.botToMqttStatus)}
onProcessPhoto={id => this.props.dispatch(scanImage(id))}
onFlip={uuid => this.props.dispatch(selectImage(uuid))}
onFlip={this.onFlip}
images={this.props.images}
currentImage={this.props.currentImage}
onChange={this.change}
@ -73,11 +91,10 @@ 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.wDEnv)} />
<WeedDetectorConfig
values={this.props.wDEnv}
onChange={this.saveEnvVar} />
</MustBeOnline>
this.props.wDEnv)} />}
<WeedDetectorConfig
values={this.props.wDEnv}
onChange={this.saveEnvVar} />
</Col>
</Row>
</div>;

View File

@ -31,18 +31,6 @@ describe("<WeedDetectorConfig />", () => {
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} />);

View File

@ -15,6 +15,9 @@ import { isNumber } from "lodash";
import { t } from "../../i18next_wrapper";
export class WeedDetectorConfig extends React.Component<SettingsMenuProps, {}> {
getValue(conf: keyof WD_ENV) { return envGet(conf, this.props.values); }
get simple() { return !!this.getValue("CAMERA_CALIBRATION_easy_calibration"); }
NumberBox = ({ conf, label }: {
conf: keyof WD_ENV;
label: string;
@ -25,7 +28,7 @@ export class WeedDetectorConfig extends React.Component<SettingsMenuProps, {}> {
</label>
<BlurableInput type="number"
id={conf}
value={"" + envGet(conf, this.props.values)}
value={"" + this.getValue(conf)}
onCommit={e =>
this.props.onChange(conf, parseFloat(e.currentTarget.value))}
placeholder={label} />
@ -40,57 +43,48 @@ export class WeedDetectorConfig extends React.Component<SettingsMenuProps, {}> {
}
};
find = (needle: keyof WD_ENV): DropDownItem => {
const wow = envGet(needle, this.props.values);
const ok = SPECIAL_VALUE_DDI[wow];
return ok || NULL_CHOICE;
};
find = (conf: keyof WD_ENV): DropDownItem =>
SPECIAL_VALUE_DDI[this.getValue(conf)] || NULL_CHOICE
render() {
return <div>
<label htmlFor="invert_hue_selection">
{t("Invert Hue Range Selection")}
</label>
<div>
<input
type="checkbox"
id="invert_hue_selection"
checked={!!envGet("CAMERA_CALIBRATION_invert_hue_selection",
this.props.values)}
onChange={e =>
this.props.onChange("CAMERA_CALIBRATION_invert_hue_selection",
e.currentTarget.checked ?
SPECIAL_VALUES.TRUE : SPECIAL_VALUES.FALSE)} />
</div>
<this.NumberBox
conf={"CAMERA_CALIBRATION_calibration_object_separation"}
label={t(`Calibration Object Separation`)} />
<label>
{t(`Calibration Object Separation along axis`)}
</label>
<FBSelect
onChange={this.setDDI("CAMERA_CALIBRATION_calibration_along_axis")}
selectedItem={this.find("CAMERA_CALIBRATION_calibration_along_axis")}
list={CALIBRATION_DROPDOWNS} />
<Row>
<Col xs={6}>
return <div className={"camera-calibration-config"}>
{!this.simple &&
<div className={"camera-calibration-configs"}>
<BoolConfig
wDEnv={this.props.values}
configKey={"CAMERA_CALIBRATION_invert_hue_selection"}
label={t("Invert Hue Range Selection")}
onChange={this.props.onChange} />
<this.NumberBox
conf={"CAMERA_CALIBRATION_camera_offset_x"}
label={t(`Camera Offset X`)} />
</Col>
<Col xs={6}>
<this.NumberBox
conf={"CAMERA_CALIBRATION_camera_offset_y"}
label={t(`Camera Offset Y`)} />
</Col>
</Row>
<label htmlFor="image_bot_origin_location">
{t(`Origin Location in Image`)}
</label>
<FBSelect
list={ORIGIN_DROPDOWNS}
onChange={this.setDDI("CAMERA_CALIBRATION_image_bot_origin_location")}
selectedItem={this.find("CAMERA_CALIBRATION_image_bot_origin_location")} />
conf={"CAMERA_CALIBRATION_calibration_object_separation"}
label={t(`Calibration Object Separation`)} />
<label>
{t(`Calibration Object Separation along axis`)}
</label>
<FBSelect
onChange={this.setDDI("CAMERA_CALIBRATION_calibration_along_axis")}
selectedItem={this.find("CAMERA_CALIBRATION_calibration_along_axis")}
list={CALIBRATION_DROPDOWNS} />
<Row>
<Col xs={6}>
<this.NumberBox
conf={"CAMERA_CALIBRATION_camera_offset_x"}
label={t(`Camera Offset X`)} />
</Col>
<Col xs={6}>
<this.NumberBox
conf={"CAMERA_CALIBRATION_camera_offset_y"}
label={t(`Camera Offset Y`)} />
</Col>
</Row>
<label htmlFor="image_bot_origin_location">
{t(`Origin Location in Image`)}
</label>
<FBSelect
list={ORIGIN_DROPDOWNS}
onChange={this.setDDI("CAMERA_CALIBRATION_image_bot_origin_location")}
selectedItem={this.find("CAMERA_CALIBRATION_image_bot_origin_location")} />
</div>}
<Row>
<Col xs={6}>
<this.NumberBox
@ -106,3 +100,25 @@ export class WeedDetectorConfig extends React.Component<SettingsMenuProps, {}> {
</div>;
}
}
export interface BoolConfigProps {
configKey: keyof WD_ENV;
label: string;
wDEnv: Partial<WD_ENV>;
onChange(key: keyof WD_ENV, value: number): void;
}
export const BoolConfig = (props: BoolConfigProps) =>
<div className="boolean-camera-calibration-config">
<label htmlFor={props.configKey}>
{t(props.label)}
</label>
<input
type="checkbox"
id={props.configKey}
checked={!!envGet(props.configKey, props.wDEnv)}
onChange={e =>
props.onChange(props.configKey,
e.currentTarget.checked ?
SPECIAL_VALUES.TRUE : SPECIAL_VALUES.FALSE)} />
</div>;

View File

@ -79,29 +79,24 @@ export class WeedDetector
</div>
<Row>
<Col sm={12}>
<MustBeOnline
syncStatus={this.props.syncStatus}
networkState={this.props.botToMqttStatus}
lockOpen={process.env.NODE_ENV !== "production"}>
<ImageWorkspace
botOnline={
isBotOnline(this.props.syncStatus, this.props.botToMqttStatus)}
onProcessPhoto={id => this.props.dispatch(scanImage(id))}
onFlip={uuid => this.props.dispatch(selectImage(uuid))}
currentImage={this.props.currentImage}
images={this.props.images}
onChange={this.change}
timeSettings={this.props.timeSettings}
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>
<ImageWorkspace
botOnline={
isBotOnline(this.props.syncStatus, this.props.botToMqttStatus)}
onProcessPhoto={id => this.props.dispatch(scanImage(id))}
onFlip={uuid => this.props.dispatch(selectImage(uuid))}
currentImage={this.props.currentImage}
images={this.props.images}
onChange={this.change}
timeSettings={this.props.timeSettings}
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"))} />
</Col>
</Row>
</div>;

View File

@ -40,6 +40,11 @@ describe("formatEnvKey()", () => {
v: SPECIAL_VALUES.FALSE,
r: "FALSE"
},
{
k: "CAMERA_CALIBRATION_easy_calibration",
v: SPECIAL_VALUES.FALSE,
r: "FALSE"
},
{
k: "CAMERA_CALIBRATION_calibration_along_axis",
v: SPECIAL_VALUES.X,

View File

@ -25,6 +25,7 @@ export const WD_KEY_DEFAULTS = {
CAMERA_CALIBRATION_calibration_along_axis: SPECIAL_VALUES.X,
CAMERA_CALIBRATION_image_bot_origin_location: SPECIAL_VALUES.BOTTOM_LEFT,
CAMERA_CALIBRATION_invert_hue_selection: SPECIAL_VALUES.TRUE,
CAMERA_CALIBRATION_easy_calibration: SPECIAL_VALUES.FALSE,
CAMERA_CALIBRATION_blur: 5,
CAMERA_CALIBRATION_calibration_object_separation: 100,
CAMERA_CALIBRATION_camera_offset_x: 50,
@ -61,6 +62,7 @@ export const DEFAULT_FORMATTER: Translation = {
case "CAMERA_CALIBRATION_calibration_along_axis":
case "CAMERA_CALIBRATION_image_bot_origin_location":
case "CAMERA_CALIBRATION_invert_hue_selection":
case "CAMERA_CALIBRATION_easy_calibration":
return ("" + (SPECIAL_VALUES[val] || val));
default:
return val;

View File

@ -24,24 +24,10 @@ describe("mapStateToProps()", () => {
state.bot.hardware.configuration.sequence_init_log = false;
const fakeApiConfig = fakeFbosConfig();
fakeApiConfig.body.sequence_init_log = true;
fakeApiConfig.body.api_migrated = true;
state.resources = buildResourceIndex([fakeApiConfig]);
const props = mapStateToProps(state);
expect(props.sourceFbosConfig("sequence_init_log")).toEqual({
value: true, consistent: false
});
});
it("bot source of FBOS settings", () => {
const state = fakeState();
state.bot.hardware.configuration.sequence_init_log = false;
const fakeApiConfig = fakeFbosConfig();
fakeApiConfig.body.sequence_init_log = true;
fakeApiConfig.body.api_migrated = false;
state.resources = buildResourceIndex([fakeApiConfig]);
const props = mapStateToProps(state);
expect(props.sourceFbosConfig("sequence_init_log")).toEqual({
value: false, consistent: true
});
});
});

View File

@ -18,7 +18,6 @@ describe("mapStateToProps()", () => {
it("returns firmware value", () => {
const state = fakeState();
const fbosConfig = fakeFbosConfig();
fbosConfig.body.api_migrated = true;
fbosConfig.body.firmware_hardware = "arduino";
state.resources = buildResourceIndex([fbosConfig]);
const props = mapStateToProps(state);

View File

@ -28,7 +28,7 @@ describe("<NavLinks />", () => {
});
it("shows links", () => {
mockDev = true;
mockDev = false;
const wrapper = mount(<NavLinks close={jest.fn()} alertCount={1} />);
expect(wrapper.text().toLowerCase()).not.toContain("tools");
});

View File

@ -37,7 +37,7 @@ export const getLinks = (): NavLinkParams[] => betterCompact([
name: "Regimens", icon: "calendar-check-o", slug: "regimens",
computeHref: computeEditorUrlFromState("Regimen")
},
DevSettings.futureFeaturesEnabled() ? undefined :
!DevSettings.futureFeaturesEnabled() ? undefined :
{ name: "Tools", icon: "wrench", slug: "tools" },
{
name: "Farmware", icon: "crosshairs", slug: "farmware",

View File

@ -1,10 +1,10 @@
import { info } from "../toast/toast";
import { semverCompare, SemverResult, MinVersionOverride } from "../util";
import { semverCompare, SemverResult, FbosVersionFallback } from "../util";
import { Content } from "../constants";
import { Dictionary } from "lodash";
const IDEAL_VERSION =
globalConfig.FBOS_END_OF_LIFE_VERSION || MinVersionOverride.ALWAYS;
globalConfig.FBOS_END_OF_LIFE_VERSION || FbosVersionFallback.NULL;
/** Returns a function that, when given a version string, (possibly) warns the
* user to upgrade FBOS versions before it hits end of life. */
@ -12,8 +12,8 @@ export function createReminderFn() {
/** FBOS Version can change during the app lifecycle. We only want one
* reminder per FBOS version change. */
const alreadyChecked: Dictionary<boolean | undefined> = {
// Dont bother when the user is offline.
[MinVersionOverride.ALWAYS]: true
// Don't bother when the user is offline.
[FbosVersionFallback.NULL]: true
};
return function reminder(version: string) {

View File

@ -1,5 +1,5 @@
import { EnvName } from "./interfaces";
import { determineInstalledOsVersion, MinVersionOverride } from "../util/index";
import { determineInstalledOsVersion, FbosVersionFallback } from "../util/index";
import { maybeGetDevice } from "../resources/selectors";
import { MW } from "./middlewares";
import { Everything } from "../interfaces";
@ -11,10 +11,10 @@ const maybeRemindUserToUpdate = createReminderFn();
function getVersionFromState(state: Everything) {
const device = maybeGetDevice(state.resources.index);
const v =
determineInstalledOsVersion(state.bot, device) || MinVersionOverride.ALWAYS;
maybeRemindUserToUpdate(v);
return v;
const version = determineInstalledOsVersion(state.bot, device)
|| FbosVersionFallback.NULL;
maybeRemindUserToUpdate(version);
return version;
}
const fn: MW =

View File

@ -5,6 +5,7 @@ import {
createShouldDisplayFn,
determineInstalledOsVersion,
versionOK,
MinVersionOverride,
} from "../version";
import { bot } from "../../__test_support__/fake_state/bot";
import { fakeDevice } from "../../__test_support__/resource_index_builder";
@ -128,6 +129,10 @@ describe("shouldDisplay()", () => {
expect(createShouldDisplayFn("10.0.0",
{ jest_feature: "1.0.0" }, undefined)(
Feature.jest_feature)).toBeTruthy();
globalConfig.FBOS_END_OF_LIFE_VERSION = MinVersionOverride.NEVER;
expect(createShouldDisplayFn(undefined, fakeMinOsData, undefined)(
Feature.jest_feature)).toBeTruthy();
delete globalConfig.FBOS_END_OF_LIFE_VERSION;
});
it("shouldn't display", () => {
@ -179,14 +184,20 @@ describe("determineInstalledOsVersion()", () => {
describe("versionOK()", () => {
it("checks if major/minor version meets min requirement", () => {
expect(versionOK("9.1.9-rc99", 3, 0)).toBeTruthy();
expect(versionOK("3.0.9-rc99", 3, 0)).toBeTruthy();
expect(versionOK("4.0.0", 3, 0)).toBeTruthy();
expect(versionOK("4.0.0", 3, 1)).toBeTruthy();
expect(versionOK("3.1.0", 3, 0)).toBeTruthy();
expect(versionOK("2.0.-", 3, 0)).toBeFalsy();
expect(versionOK("2.9.4", 3, 0)).toBeFalsy();
expect(versionOK("1.9.6", 3, 0)).toBeFalsy();
expect(versionOK("3.1.6", 4, 0)).toBeFalsy();
globalConfig.MINIMUM_FBOS_VERSION = "3.0.0";
expect(versionOK("9.1.9-rc99")).toBeTruthy();
expect(versionOK("3.0.9-rc99")).toBeTruthy();
expect(versionOK("4.0.0")).toBeTruthy();
expect(versionOK("3.1.0")).toBeTruthy();
expect(versionOK("2.0.-")).toBeFalsy();
expect(versionOK("2.9.4")).toBeFalsy();
expect(versionOK("1.9.6")).toBeFalsy();
globalConfig.MINIMUM_FBOS_VERSION = "3.1.0";
expect(versionOK("4.0.0")).toBeTruthy();
globalConfig.MINIMUM_FBOS_VERSION = "4.0.0";
expect(versionOK("3.1.6")).toBeFalsy();
delete globalConfig.MINIMUM_FBOS_VERSION;
expect(versionOK("5.0.0")).toBeFalsy();
expect(versionOK("7.0.0")).toBeTruthy();
});
});

View File

@ -183,9 +183,7 @@ export function validBotLocationData(
*/
export function validFwConfig(config: TaggedFirmwareConfig | undefined):
TaggedFirmwareConfig["body"] | undefined {
return (config?.body.api_migrated)
? config.body
: undefined;
return config ? config.body : undefined;
}
/**
@ -193,9 +191,7 @@ export function validFwConfig(config: TaggedFirmwareConfig | undefined):
*/
export function validFbosConfig(
config: TaggedFbosConfig | undefined): TaggedFbosConfig["body"] | undefined {
return (config?.body.api_migrated)
? config.body
: undefined;
return config ? config.body : undefined;
}
interface BetterUUID {

View File

@ -97,10 +97,13 @@ export function minFwVersionCheck(current: string | undefined, min: string) {
* for shouldDisplay()
*/
export enum MinVersionOverride {
ALWAYS = "0.0.0",
NEVER = "999.999.999",
}
export enum FbosVersionFallback {
NULL = "0.0.0",
}
/**
* Determine whether a feature should be displayed based on
* the user's current FBOS version. Min FBOS version feature data is pulled
@ -114,19 +117,18 @@ export function createShouldDisplayFn(
lookupData: MinOsFeatureLookup | undefined,
override: string | undefined) {
return function (feature: Feature): boolean {
const target = override || current;
if (isString(target)) {
const table = lookupData || {};
const min = table[feature] || MinVersionOverride.NEVER;
switch (semverCompare(target, min)) {
case SemverResult.LEFT_IS_GREATER:
case SemverResult.EQUAL:
return true;
default:
return false;
}
const fallback = globalConfig.FBOS_END_OF_LIFE_VERSION ||
FbosVersionFallback.NULL;
const target = override || current || fallback;
const table = lookupData || {};
const min = table[feature] || MinVersionOverride.NEVER;
switch (semverCompare(target, min)) {
case SemverResult.LEFT_IS_GREATER:
case SemverResult.EQUAL:
return true;
default:
return false;
}
return false;
};
}
@ -147,6 +149,9 @@ export function determineInstalledOsVersion(
}
}
const parseVersion = (version: string) =>
version.split(".").map(x => parseInt(x, 10));
/**
* Compare installed FBOS version against the lowest version compatible
* with the web app to lock out incompatible FBOS versions from the App.
@ -155,20 +160,16 @@ export function determineInstalledOsVersion(
* identifiers.
*
* @param stringyVersion version string to check ("0.0.0")
* @param _EXPECTED_MAJOR minimum required major version number
* @param _EXPECTED_MINOR minimum required minor version number
*/
export function versionOK(stringyVersion = "0.0.0",
_EXPECTED_MAJOR: number,
_EXPECTED_MINOR: number) {
const [actual_major, actual_minor] = stringyVersion
.split(".")
.map(x => parseInt(x, 10));
if (actual_major > _EXPECTED_MAJOR) {
export function versionOK(stringyVersion = "0.0.0") {
const [actual_major, actual_minor] = parseVersion(stringyVersion);
const [EXPECTED_MAJOR, EXPECTED_MINOR] =
parseVersion(globalConfig.MINIMUM_FBOS_VERSION || "6.0.0");
if (actual_major > EXPECTED_MAJOR) {
return true;
} else {
const majorOK = (actual_major == _EXPECTED_MAJOR);
const minorOK = (actual_minor >= _EXPECTED_MINOR);
const majorOK = (actual_major == EXPECTED_MAJOR);
const minorOK = (actual_minor >= EXPECTED_MINOR);
return (majorOK && minorOK);
}
}

View File

@ -101,6 +101,14 @@ describe Api::DevicesController do
device.tool_slots.order(id: :asc)[5]
end
def tool_slots_slot_7?(device)
device.tool_slots.order(id: :asc)[6]
end
def tool_slots_slot_8?(device)
device.tool_slots.order(id: :asc)[7]
end
def tools_seed_bin?(device)
device.tools.find_by(name: "Seed Bin")
end
@ -117,10 +125,6 @@ describe Api::DevicesController do
device.tools.find_by(name: "Seed Trough 2")
end
def tools_seed_trough_3?(device)
device.tools.find_by(name: "Seed Trough 3")
end
def tools_seeder?(device)
device.tools.find_by(name: "Seeder")
end
@ -230,17 +234,20 @@ describe Api::DevicesController do
expect(tool_slots_slot_4?(device).name).to eq("Watering Nozzle")
expect(tool_slots_slot_5?(device).name).to eq("Soil Sensor")
expect(tool_slots_slot_6?(device).name).to eq("Weeder")
expect(tool_slots_slot_7?(device)).to_not be
expect(tool_slots_slot_8?(device)).to_not be
check_slot_pairing(tool_slots_slot_1?(device), "Seeder")
check_slot_pairing(tool_slots_slot_2?(device), "Seed Bin")
check_slot_pairing(tool_slots_slot_3?(device), "Seed Tray")
check_slot_pairing(tool_slots_slot_4?(device), "Watering Nozzle")
check_slot_pairing(tool_slots_slot_5?(device), "Soil Sensor")
check_slot_pairing(tool_slots_slot_6?(device), "Weeder")
expect(tools_seed_bin?(device)).to be
expect(tools_seed_tray?(device)).to be
expect(tools_seed_trough_1?(device)).to_not be
expect(tools_seed_trough_2?(device)).to_not be
expect(tools_seed_trough_3?(device)).to_not be
expect(tools_seeder?(device)).to be_kind_of(Tool)
expect(tools_soil_sensor?(device)).to be_kind_of(Tool)
expect(tools_watering_nozzle?(device)).to be_kind_of(Tool)
@ -280,6 +287,8 @@ describe Api::DevicesController do
expect(tool_slots_slot_4?(device).name).to eq("Watering Nozzle")
expect(tool_slots_slot_5?(device).name).to eq("Soil Sensor")
expect(tool_slots_slot_6?(device).name).to eq("Weeder")
expect(tool_slots_slot_7?(device)).to_not be
expect(tool_slots_slot_8?(device)).to_not be
check_slot_pairing(tool_slots_slot_1?(device), "Seeder")
check_slot_pairing(tool_slots_slot_2?(device), "Seed Bin")
@ -287,11 +296,11 @@ describe Api::DevicesController do
check_slot_pairing(tool_slots_slot_4?(device), "Watering Nozzle")
check_slot_pairing(tool_slots_slot_5?(device), "Soil Sensor")
check_slot_pairing(tool_slots_slot_6?(device), "Weeder")
expect(tools_seed_bin?(device)).to be
expect(tools_seed_tray?(device)).to be
expect(tools_seed_trough_1?(device)).to_not be
expect(tools_seed_trough_2?(device)).to_not be
expect(tools_seed_trough_3?(device)).to_not be
expect(tools_seeder?(device)).to be_kind_of(Tool)
expect(tools_soil_sensor?(device)).to be_kind_of(Tool)
expect(tools_watering_nozzle?(device)).to be_kind_of(Tool)
@ -331,11 +340,20 @@ describe Api::DevicesController do
expect(tool_slots_slot_4?(device).name).to eq("Watering Nozzle")
expect(tool_slots_slot_5?(device).name).to eq("Soil Sensor")
expect(tool_slots_slot_6?(device).name).to eq("Weeder")
expect(tool_slots_slot_7?(device)).to_not be
expect(tool_slots_slot_8?(device)).to_not be
check_slot_pairing(tool_slots_slot_1?(device), "Seeder")
check_slot_pairing(tool_slots_slot_2?(device), "Seed Bin")
check_slot_pairing(tool_slots_slot_3?(device), "Seed Tray")
check_slot_pairing(tool_slots_slot_4?(device), "Watering Nozzle")
check_slot_pairing(tool_slots_slot_5?(device), "Soil Sensor")
check_slot_pairing(tool_slots_slot_6?(device), "Weeder")
expect(tools_seed_bin?(device)).to be
expect(tools_seed_tray?(device)).to be
expect(tools_seed_trough_1?(device)).to_not be
expect(tools_seed_trough_2?(device)).to_not be
expect(tools_seed_trough_3?(device)).to_not be
expect(tools_seeder?(device)).to be_kind_of(Tool)
expect(tools_soil_sensor?(device)).to be_kind_of(Tool)
expect(tools_watering_nozzle?(device)).to be_kind_of(Tool)
@ -375,11 +393,22 @@ describe Api::DevicesController do
expect(tool_slots_slot_4?(device).name).to eq("Watering Nozzle")
expect(tool_slots_slot_5?(device).name).to eq("Soil Sensor")
expect(tool_slots_slot_6?(device).name).to eq("Weeder")
expect(tool_slots_slot_7?(device).name).to eq("Seed Trough 1")
expect(tool_slots_slot_8?(device).name).to eq("Seed Trough 2")
check_slot_pairing(tool_slots_slot_1?(device), "Seeder")
check_slot_pairing(tool_slots_slot_2?(device), "Seed Bin")
check_slot_pairing(tool_slots_slot_3?(device), "Seed Tray")
check_slot_pairing(tool_slots_slot_4?(device), "Watering Nozzle")
check_slot_pairing(tool_slots_slot_5?(device), "Soil Sensor")
check_slot_pairing(tool_slots_slot_6?(device), "Weeder")
check_slot_pairing(tool_slots_slot_7?(device), "Seed Trough 1")
check_slot_pairing(tool_slots_slot_8?(device), "Seed Trough 2")
expect(tools_seed_bin?(device)).to be
expect(tools_seed_tray?(device)).to be
expect(tools_seed_trough_1?(device)).to_not be
expect(tools_seed_trough_2?(device)).to_not be
expect(tools_seed_trough_3?(device)).to_not be
expect(tools_seed_trough_1?(device)).to be
expect(tools_seed_trough_2?(device)).to be
expect(tools_seeder?(device)).to be_kind_of(Tool)
expect(tools_soil_sensor?(device)).to be_kind_of(Tool)
expect(tools_watering_nozzle?(device)).to be_kind_of(Tool)
@ -419,6 +448,8 @@ describe Api::DevicesController do
expect(tool_slots_slot_4?(device).name).to eq("Watering Nozzle")
expect(tool_slots_slot_5?(device).name).to eq("Soil Sensor")
expect(tool_slots_slot_6?(device).name).to eq("Weeder")
expect(tool_slots_slot_7?(device)).to_not be
expect(tool_slots_slot_8?(device)).to_not be
check_slot_pairing(tool_slots_slot_1?(device), "Seeder")
check_slot_pairing(tool_slots_slot_2?(device), "Seed Bin")
@ -431,7 +462,6 @@ describe Api::DevicesController do
expect(tools_seed_tray?(device)).to be
expect(tools_seed_trough_1?(device)).to_not be
expect(tools_seed_trough_2?(device)).to_not be
expect(tools_seed_trough_3?(device)).to_not be
expect(tools_seeder?(device)).to be_kind_of(Tool)
expect(tools_soil_sensor?(device)).to be_kind_of(Tool)
expect(tools_watering_nozzle?(device)).to be_kind_of(Tool)
@ -471,6 +501,8 @@ describe Api::DevicesController do
expect(tool_slots_slot_4?(device).name).to eq("Watering Nozzle")
expect(tool_slots_slot_5?(device).name).to eq("Soil Sensor")
expect(tool_slots_slot_6?(device).name).to eq("Weeder")
expect(tool_slots_slot_7?(device).name).to eq("Seed Trough 1")
expect(tool_slots_slot_8?(device).name).to eq("Seed Trough 2")
check_slot_pairing(tool_slots_slot_1?(device), "Seeder")
check_slot_pairing(tool_slots_slot_2?(device), "Seed Bin")
@ -478,12 +510,13 @@ describe Api::DevicesController do
check_slot_pairing(tool_slots_slot_4?(device), "Watering Nozzle")
check_slot_pairing(tool_slots_slot_5?(device), "Soil Sensor")
check_slot_pairing(tool_slots_slot_6?(device), "Weeder")
check_slot_pairing(tool_slots_slot_7?(device), "Seed Trough 1")
check_slot_pairing(tool_slots_slot_8?(device), "Seed Trough 2")
expect(tools_seed_bin?(device)).to be
expect(tools_seed_tray?(device)).to be
expect(tools_seed_trough_1?(device)).to_not be
expect(tools_seed_trough_2?(device)).to_not be
expect(tools_seed_trough_3?(device)).to_not be
expect(tools_seed_trough_1?(device)).to be
expect(tools_seed_trough_2?(device)).to be
expect(tools_seeder?(device)).to be_kind_of(Tool)
expect(tools_soil_sensor?(device)).to be_kind_of(Tool)
expect(tools_watering_nozzle?(device)).to be_kind_of(Tool)
@ -519,18 +552,20 @@ describe Api::DevicesController do
expect(settings_hide_sensors?(device)).to be(true)
expect(tool_slots_slot_1?(device).name).to eq("Seed Trough 1")
expect(tool_slots_slot_2?(device).name).to eq("Seed Trough 2")
expect(tool_slots_slot_3?(device).name).to eq("Seed Trough 3")
expect(tool_slots_slot_3?(device)).to_not be
expect(tool_slots_slot_4?(device)).to_not be
expect(tool_slots_slot_5?(device)).to_not be
expect(tool_slots_slot_6?(device)).to_not be
expect(tool_slots_slot_7?(device)).to_not be
expect(tool_slots_slot_8?(device)).to_not be
check_slot_pairing(tool_slots_slot_1?(device), "Seed Trough 1")
check_slot_pairing(tool_slots_slot_2?(device), "Seed Trough 2")
check_slot_pairing(tool_slots_slot_3?(device), "Seed Trough 3")
expect(tools_seed_bin?(device)).to_not be
expect(tools_seed_tray?(device)).to_not be
expect(tools_seed_trough_1?(device)).to be
expect(tools_seed_trough_2?(device)).to be
expect(tools_seed_trough_3?(device)).to be
expect(tools_seeder?(device)).to_not be
expect(tools_soil_sensor?(device)).to_not be
expect(tools_watering_nozzle?(device)).to_not be
@ -566,18 +601,20 @@ describe Api::DevicesController do
expect(settings_hide_sensors?(device)).to be(true)
expect(tool_slots_slot_1?(device).name).to eq("Seed Trough 1")
expect(tool_slots_slot_2?(device).name).to eq("Seed Trough 2")
expect(tool_slots_slot_3?(device).name).to eq("Seed Trough 3")
expect(tool_slots_slot_3?(device)).to_not be
expect(tool_slots_slot_4?(device)).to_not be
expect(tool_slots_slot_5?(device)).to_not be
expect(tool_slots_slot_6?(device)).to_not be
expect(tool_slots_slot_7?(device)).to_not be
expect(tool_slots_slot_8?(device)).to_not be
check_slot_pairing(tool_slots_slot_1?(device), "Seed Trough 1")
check_slot_pairing(tool_slots_slot_2?(device), "Seed Trough 2")
check_slot_pairing(tool_slots_slot_3?(device), "Seed Trough 3")
expect(tools_seed_bin?(device)).to_not be
expect(tools_seed_tray?(device)).to_not be
expect(tools_seed_trough_1?(device)).to be
expect(tools_seed_trough_2?(device)).to be
expect(tools_seed_trough_3?(device)).to be
expect(tools_seeder?(device)).to_not be
expect(tools_soil_sensor?(device)).to_not be
expect(tools_watering_nozzle?(device)).to_not be