Merge pull request #1698 from FarmBot/model_updates

Settings and version updates
pull/1699/head
Rick Carlino 2020-02-16 08:25:23 -06:00 committed by GitHub
commit 310686508f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
131 changed files with 1527 additions and 1072 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

@ -4,7 +4,10 @@ export const panelState = (): ControlPanelState => {
return {
homing_and_calibration: false,
motors: false,
encoders_and_endstops: false,
encoders: false,
endstops: false,
error_handling: false,
pin_bindings: false,
danger_zone: false,
power_and_reset: false,
pin_guard: false

View File

@ -4,12 +4,15 @@ export const bot: Everything["bot"] = {
"consistent": true,
"stepSize": 100,
"controlPanelState": {
"homing_and_calibration": false,
"motors": false,
"encoders_and_endstops": false,
"danger_zone": false,
"power_and_reset": false,
"pin_guard": false,
homing_and_calibration: false,
motors: false,
encoders: false,
endstops: false,
error_handling: false,
pin_bindings: false,
danger_zone: false,
power_and_reset: false,
pin_guard: false,
},
"hardware": {
"gpio_registry": {},

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

@ -0,0 +1,33 @@
jest.unmock("../external_urls");
import { ExternalUrl } from "../external_urls";
/* tslint:disable:max-line-length */
describe("ExternalUrl", () => {
it("returns urls", () => {
expect(ExternalUrl.featureMinVersions)
.toEqual("https://raw.githubusercontent.com/FarmBot/farmbot_os/FEATURE_MIN_VERSIONS.json");
expect(ExternalUrl.osReleaseNotes)
.toEqual("https://raw.githubusercontent.com/FarmBot/farmbot_os/RELEASE_NOTES.md");
expect(ExternalUrl.latestRelease)
.toEqual("https://api.github.com/repos/FarmBot/farmbot_os/releases/latest");
expect(ExternalUrl.webAppRepo)
.toEqual("https://github.com/FarmBot/Farmbot-Web-App");
expect(ExternalUrl.gitHubFarmBot)
.toEqual("https://github.com/FarmBot");
expect(ExternalUrl.softwareDocs)
.toEqual("https://software.farm.bot/docs");
expect(ExternalUrl.softwareForum)
.toEqual("http://forum.farmbot.org/c/software");
expect(ExternalUrl.OpenFarm.cropApi)
.toEqual("https://openfarm.cc/api/v1/crops/");
expect(ExternalUrl.OpenFarm.cropBrowse)
.toEqual("https://openfarm.cc/crops/");
expect(ExternalUrl.OpenFarm.newCrop)
.toEqual("https://openfarm.cc/en/crops/new");
expect(ExternalUrl.Videos.desktop)
.toEqual("https://cdn.shopify.com/s/files/1/2040/0289/files/Farm_Designer_Loop.mp4?9552037556691879018");
expect(ExternalUrl.Videos.mobile)
.toEqual("https://cdn.shopify.com/s/files/1/2040/0289/files/Controls.png?9668345515035078097");
});
});

View File

@ -158,6 +158,10 @@ export class API {
get farmwareInstallationPath() {
return `${this.baseUrl}/api/farmware_installations/`;
}
/** /api/first_party_farmwares */
get firstPartyFarmwarePath() {
return `${this.baseUrl}/api/first_party_farmwares`;
}
/** /api/alerts/:id */
get alertPath() { return `${this.baseUrl}/api/alerts/`; }
/** /api/global_bulletins/:id */

View File

@ -1,5 +1,6 @@
import * as React from "react";
import { Session } from "./session";
import { ExternalUrl } from "./external_urls";
const OUTER_STYLE: React.CSSProperties = {
borderRadius: "10px",
@ -47,7 +48,7 @@ export function Apology(_: {}) {
<li>
<span>
Send a report to our developer team via the&nbsp;
<a href="http://forum.farmbot.org/c/software">FarmBot software
<a href={ExternalUrl.softwareForum}>FarmBot software
forum</a>. Including additional information (such as steps leading up
to the error) helps us identify solutions more quickly.
</span>

View File

@ -1,6 +1,6 @@
import axios from "axios";
import {
fetchReleases, fetchMinOsFeatureData, FEATURE_MIN_VERSIONS_URL,
fetchReleases, fetchMinOsFeatureData,
fetchLatestGHBetaRelease
} from "../devices/actions";
import { AuthState } from "./interfaces";
@ -16,6 +16,7 @@ import { Actions } from "../constants";
import { connectDevice } from "../connectivity/connect_device";
import { getFirstPartyFarmwareList } from "../farmware/actions";
import { readOnlyInterceptor } from "../read_only_mode";
import { ExternalUrl } from "../external_urls";
export function didLogin(authState: AuthState, dispatch: Function) {
API.setBaseUrl(authState.token.unencoded.iss);
@ -24,7 +25,7 @@ export function didLogin(authState: AuthState, dispatch: Function) {
beta_os_update_server && beta_os_update_server != "NOT_SET" &&
dispatch(fetchLatestGHBetaRelease(beta_os_update_server));
dispatch(getFirstPartyFarmwareList());
dispatch(fetchMinOsFeatureData(FEATURE_MIN_VERSIONS_URL));
dispatch(fetchMinOsFeatureData(ExternalUrl.featureMinVersions));
dispatch(setToken(authState));
Sync.fetchSyncData(dispatch);
dispatch(connectDevice(authState));

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

@ -39,8 +39,8 @@ export namespace ToolTips {
few sequences to verify that everything works as expected.`);
export const PIN_BINDINGS =
trim(`Assign a sequence to execute when a Raspberry Pi GPIO pin is
activated.`);
trim(`Assign an action or sequence to execute when a Raspberry Pi
GPIO pin is activated.`);
export const PIN_BINDING_WARNING =
trim(`Warning: Binding to a pin without a physical button and
@ -51,24 +51,38 @@ export namespace ToolTips {
trim(`Diagnose connectivity issues with FarmBot and the browser.`);
// Hardware Settings: Homing and Calibration
export const HOMING =
export const HOMING_ENCODERS =
trim(`If encoders or end-stops are enabled, home axis (find zero).`);
export const CALIBRATION =
export const HOMING_STALL_DETECTION =
trim(`If stall detection or end-stops are enabled, home axis
(find zero).`);
export const CALIBRATION_ENCODERS =
trim(`If encoders or end-stops are enabled, home axis and determine
maximum.`);
export const CALIBRATION_STALL_DETECTION =
trim(`If stall detection or end-stops are enabled, home axis and
determine maximum.`);
export const SET_ZERO_POSITION =
trim(`Set the current location as zero.`);
export const FIND_HOME_ON_BOOT =
export const FIND_HOME_ON_BOOT_ENCODERS =
trim(`If encoders or end-stops are enabled, find the home position
when the device powers on.
Warning! This will perform homing on all axes when the
device powers on. Encoders or endstops must be enabled.
when the device powers on. Warning! This will perform homing on all
axes when the device powers on. Encoders or endstops must be enabled.
It is recommended to make sure homing works properly before enabling
this feature. (default: disabled)`);
export const FIND_HOME_ON_BOOT_STALL_DETECTION =
trim(`If stall detection or end-stops are enabled, find the home
position when the device powers on. Warning! This will perform homing
on all axes when the device powers on. Stall detection or endstops
must be enabled. It is recommended to make sure homing works properly
before enabling this feature. (default: disabled)`);
export const STOP_AT_HOME =
trim(`Stop at the home location of the axis. (default: disabled)`);
@ -85,18 +99,7 @@ export namespace ToolTips {
trim(`Set the length of each axis to provide software limits.
Used only if STOP AT MAX is enabled. (default: 0 (disabled))`);
export const TIMEOUT_AFTER =
trim(`Amount of time to wait for a command to execute before stopping.
(default: 120s)`);
// Hardware Settings: Motors
export const MAX_MOVEMENT_RETRIES =
trim(`Number of times to retry a movement before stopping. (default: 3)`);
export const E_STOP_ON_MOV_ERR =
trim(`Emergency stop if movement is not complete after the maximum
number of retries. (default: disabled)`);
export const MAX_SPEED =
trim(`Maximum travel speed after acceleration in millimeters per second.
(default: x: 80mm/s, y: 80mm/s, z: 16mm/s)`);
@ -132,18 +135,22 @@ export namespace ToolTips {
export const MOTOR_CURRENT =
trim(`Motor current in milliamps. (default: 600)`);
export const STALL_SENSITIVITY =
trim(`Motor stall sensitivity. (default: 30)`);
export const ENABLE_X2_MOTOR =
trim(`Enable use of a second x-axis motor. Connects to E0 on RAMPS.
(default: enabled)`);
// Hardware Settings: Encoders and Endstops
// Hardware Settings: Encoders / Stall Detection
export const ENABLE_ENCODERS =
trim(`Enable use of rotary encoders for stall detection,
calibration and homing. (default: enabled)`);
export const ENABLE_STALL_DETECTION =
trim(`Enable use of motor stall detection for detecting missed steps,
calibration and homing. (default: enabled)`);
export const STALL_SENSITIVITY =
trim(`Motor stall sensitivity. (default: 30)`);
export const ENCODER_POSITIONING =
trim(`Use encoders for positioning. (default: disabled)`);
@ -151,17 +158,22 @@ export namespace ToolTips {
trim(`Reverse the direction of encoder position reading.
(default: disabled)`);
export const MAX_MISSED_STEPS =
export const MAX_MISSED_STEPS_ENCODERS =
trim(`Number of steps missed (determined by encoder) before motor is
considered to have stalled. (default: 5)`);
export const ENCODER_MISSED_STEP_DECAY =
export const MAX_MISSED_STEPS_STALL_DETECTION =
trim(`Number of steps missed (determined by motor stall detection) before
motor is considered to have stalled. (default: 5)`);
export const MISSED_STEP_DECAY =
trim(`Reduction to missed step total for every good step. (default: 5)`);
export const ENCODER_SCALING =
trim(`encoder scaling factor = 10000 * (motor resolution * microsteps)
/ (encoder resolution). (default: 5556 (10000*200/360))`);
// Hardware Settings: Endstops
export const ENABLE_ENDSTOPS =
trim(`Enable use of electronic end-stops for end detection,
calibration and homing. (default: disabled)`);
@ -173,6 +185,18 @@ export namespace ToolTips {
trim(`Invert axis end-stops. Enable for normally closed (NC),
disable for normally open (NO). (default: disabled)`);
// Hardware Settings: Error Handling
export const TIMEOUT_AFTER =
trim(`Amount of time to wait for a command to execute before stopping.
(default: 120s)`);
export const MAX_MOVEMENT_RETRIES =
trim(`Number of times to retry a movement before stopping. (default: 3)`);
export const E_STOP_ON_MOV_ERR =
trim(`Emergency stop if movement is not complete after the maximum
number of retries. (default: disabled)`);
// Hardware Settings: Pin Guard
export const PIN_GUARD_PIN_NUMBER =
trim(`The number of the pin to guard. This pin will be set to the specified
@ -263,8 +287,12 @@ export namespace ToolTips {
export const FIND_HOME =
trim(`The Find Home step instructs the device to perform a homing
command (using encoders or endstops) to find and set zero for
the chosen axis or axes.`);
command (using encoders, stall detection, or endstops) to find and set
zero for the chosen axis or axes.`);
export const CALIBRATE =
trim(`If encoders, stall detection, or end-stops are enabled,
home axis and determine maximum.`);
export const IF =
trim(`Execute a sequence if a condition is satisfied. If the condition
@ -715,8 +743,8 @@ export namespace Content {
export const END_DETECTION_DISABLED =
trim(`This command will not execute correctly because you do not have
encoders or endstops enabled for the chosen axis. Enable endstops or
encoders from the Device page for: `);
encoders, stall detection, or endstops enabled for the chosen axis.
Enable endstops, encoders, or stall detection from the Device page for: `);
export const IN_USE =
trim(`Used in another resource. Protected from deletion.`);
@ -924,8 +952,7 @@ export namespace DiagnosticMessages {
but we have no recent record of FarmBot connecting to the internet.
This usually happens because of poor WiFi connectivity in the garden,
a bad password during configuration, a very long power outage, or
blocked ports on FarmBot's local network. Please refer IT staff to
https://software.farm.bot/docs/for-it-security-professionals`);
blocked ports on FarmBot's local network. Please refer IT staff to:`);
export const NO_WS_AVAILABLE = trim(`You are either offline, using a web
browser that does not support WebSockets, or are behind a firewall that

View File

@ -2,6 +2,7 @@ import * as React from "react";
import { get } from "lodash";
import { Page } from "./ui/index";
import { Session } from "./session";
import { ExternalUrl } from "./external_urls";
/** Use currying to pass down `error` object for now. */
export function crashPage(error: object) {
@ -24,7 +25,7 @@ export function crashPage(error: object) {
<li>Perform a "hard refresh" (<strong>CTRL + SHIFT + R</strong> on most machines).</li>
<li><span><a onClick={() => Session.clear()}>Log out by clicking here.</a></span></li>
<li>Send the error information (below) to our developer team via the
<a href="http://forum.farmbot.org/c/software">FarmBot software
<a href={ExternalUrl.softwareForum}>FarmBot software
forum</a>. Including additional information (such as steps leading up
to the error) help us identify solutions more quickly. </li>
</ol>

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

@ -433,14 +433,17 @@ a {
}
}
.pin-bindings-widget {
.pin-bindings {
.fa-exclamation-triangle {
color: $orange;
margin-left: 1rem;
margin-top: 0.75rem;
}
.fa-th-large {
position: absolute;
top: 0.75rem;
left: 0.5rem;
color: $dark_gray;
margin-top: 0.5rem;
margin-left: 0.5rem;
}
.fb-button {
&.green {
@ -449,16 +452,27 @@ a {
}
.bindings-list {
margin-bottom: 1rem;
margin-left: 1rem;
font-size: 1.2rem;
}
.binding-type-dropdown {
margin-bottom: 1.5rem;
}
.stock-pin-bindings-button {
button {
margin: 0 !important;
margin: 1rem;
float: left;
margin-left: 2rem;
}
i {
margin-right: 0.5rem;
}
}
.bp3-popover-wrapper {
display: inline;
float: none !important;
margin-left: 1rem;
}
}
.sensor-history-widget {
@ -1308,6 +1322,12 @@ ul {
}
}
.boolean-camera-calibration-config {
input[type=checkbox] {
display: block;
}
}
.tour-list {
margin: auto;
max-width: 300px;
@ -1519,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

@ -2,16 +2,13 @@ import { connect, MqttClient } from "mqtt";
import React from "react";
import { uuid } from "farmbot";
import axios from "axios";
import { ExternalUrl } from "../external_urls";
interface State {
error: Error | undefined;
stage: string;
}
const VIDEO_URL =
"https://cdn.shopify.com/s/files/1/2040/0289/files/Farm_Designer_Loop.mp4?9552037556691879018";
const PHONE_URL =
"https://cdn.shopify.com/s/files/1/2040/0289/files/Controls.png?9668345515035078097";
const WS_CONFIG = {
username: "farmbot_demo",
password: "required, but not used.",
@ -63,9 +60,9 @@ export class DemoIframe extends React.Component<{}, State> {
return <div className="demo-container">
<video muted={true} autoPlay={true} loop={true} className="demo-video">
<source src={VIDEO_URL} type="video/mp4" />
<source src={ExternalUrl.Videos.desktop} type="video/mp4" />
</video>
<img className="demo-phone" src={PHONE_URL} />
<img className="demo-phone" src={ExternalUrl.Videos.mobile} />
<button className="demo-button" onClick={this.requestAccount}>
{this.state.stage}
</button>

View File

@ -307,7 +307,7 @@ describe("commandErr()", () => {
});
});
describe("toggleControlPanel()", function () {
describe("toggleControlPanel()", () => {
it("toggles", () => {
const action = actions.toggleControlPanel("homing_and_calibration");
expect(action.payload).toEqual("homing_and_calibration");

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,11 +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";
// Already filtering messages in FarmBot OS and the API- this is just for
// an additional layer of safety.
const BAD_WORDS = ["WPA", "PSK", "PASSWORD", "NERVES"];
@ -132,7 +127,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";
@ -15,6 +15,7 @@ import { AutoUpdateRow } from "./fbos_settings/auto_update_row";
import { AutoSyncRow } from "./fbos_settings/auto_sync_row";
import { PowerAndReset } from "./fbos_settings/power_and_reset";
import { BootSequenceSelector } from "./fbos_settings/boot_sequence_selector";
import { ExternalUrl } from "../../external_urls";
export enum ColWidth {
label = 3,
@ -22,15 +23,12 @@ export enum ColWidth {
button = 2
}
const OS_RELEASE_NOTES_URL =
"https://raw.githubusercontent.com/FarmBot/farmbot_os/staging/RELEASE_NOTES.md";
export class FarmbotOsSettings
extends React.Component<FarmbotOsProps, FarmbotOsState> {
state: FarmbotOsState = { allOsReleaseNotes: "" };
componentDidMount() {
this.fetchReleaseNotes(OS_RELEASE_NOTES_URL);
this.fetchReleaseNotes(ExternalUrl.osReleaseNotes);
}
get osMajorVersion() {
@ -116,54 +114,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}
shouldDisplay={this.props.shouldDisplay}
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

@ -25,30 +25,25 @@ describe("<PowerAndReset/>", () => {
const state = fakeState();
state.resources = buildResourceIndex([fakeConfig]);
const fakeProps = (): PowerAndResetProps => {
return {
controlPanelState: panelState(),
dispatch: jest.fn(x => x(jest.fn(), () => state)),
sourceFbosConfig: () => ({ value: true, consistent: true }),
shouldDisplay: jest.fn(),
botOnline: true,
};
};
const fakeProps = (): PowerAndResetProps => ({
controlPanelState: panelState(),
dispatch: jest.fn(x => x(jest.fn(), () => state)),
sourceFbosConfig: () => ({ value: true, consistent: true }),
botOnline: true,
});
it("open", () => {
it("renders in open state", () => {
const p = fakeProps();
p.controlPanelState.power_and_reset = true;
const wrapper = mount(<PowerAndReset {...p} />);
["Power and Reset", "Restart", "Shutdown", "Factory Reset",
"Automatic Factory Reset", "Connection Attempt Period", "Change Ownership"]
["Power and Reset", "Restart", "Shutdown", "Restart Firmware",
"Factory Reset", "Automatic Factory Reset",
"Connection Attempt Period", "Change Ownership"]
.map(string => expect(wrapper.text().toLowerCase())
.toContain(string.toLowerCase()));
["Restart Firmware"]
.map(string => expect(wrapper.text().toLowerCase())
.not.toContain(string.toLowerCase()));
});
it("closed", () => {
it("renders as closed", () => {
const p = fakeProps();
p.controlPanelState.power_and_reset = false;
const wrapper = mount(<PowerAndReset {...p} />);
@ -73,7 +68,7 @@ describe("<PowerAndReset/>", () => {
p.sourceFbosConfig = () => ({ value: false, consistent: true });
p.controlPanelState.power_and_reset = true;
const wrapper = mount(<PowerAndReset {...p} />);
clickButton(wrapper, 3, "yes");
clickButton(wrapper, 4, "yes");
expect(edit).toHaveBeenCalledWith(fakeConfig, { disable_factory_reset: true });
expect(save).toHaveBeenCalledWith(fakeConfig.uuid);
});
@ -81,7 +76,6 @@ describe("<PowerAndReset/>", () => {
it("restarts firmware", () => {
const p = fakeProps();
p.controlPanelState.power_and_reset = true;
p.shouldDisplay = () => true;
const wrapper = mount(<PowerAndReset {...p} />);
expect(wrapper.text().toLowerCase())
.toContain("Restart Firmware".toLowerCase());

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

@ -14,6 +14,7 @@ import { timeFormatString } from "../../../util";
import { TimeSettings } from "../../../interfaces";
import { StringConfigKey } from "farmbot/dist/resources/configs/fbos";
import { boardType, FIRMWARE_CHOICES_DDI } from "../firmware_hardware_support";
import { ExternalUrl, FarmBotRepo } from "../../../external_urls";
/** Return an indicator color for the given temperature (C). */
export const colorFromTemp = (temp: number | undefined): string => {
@ -170,7 +171,7 @@ const shortenCommit = (longCommit: string) => (longCommit || "").slice(0, 8);
interface CommitDisplayProps {
title: string;
repo: string;
repo: FarmBotRepo;
commit: string;
}
@ -184,7 +185,7 @@ const CommitDisplay = (
{shortCommit === "---"
? shortCommit
: <a
href={`https://github.com/FarmBot/${repo}/tree/${shortCommit}`}
href={`${ExternalUrl.gitHubFarmBot}/${repo}/tree/${shortCommit}`}
target="_blank">
{shortCommit}
</a>}
@ -270,14 +271,15 @@ export function FbosDetails(props: FbosDetailsProps) {
timeSettings={props.timeSettings}
device={props.deviceAccount} />
<p><b>{t("Environment")}: </b>{env}</p>
<CommitDisplay title={t("Commit")} repo={"farmbot_os"} commit={commit} />
<CommitDisplay title={t("Commit")}
repo={FarmBotRepo.FarmBotOS} commit={commit} />
<p><b>{t("Target")}: </b>{target}</p>
<p><b>{t("Node name")}: </b>{last((node_name || "").split("@"))}</p>
<p><b>{t("Device ID")}: </b>{props.deviceAccount.body.id}</p>
{isString(private_ip) && <p><b>{t("Local IP address")}: </b>{private_ip}</p>}
<p><b>{t("Firmware")}: </b>{reformatFwVersion(firmware_version)}</p>
<CommitDisplay title={t("Firmware commit")}
repo={"farmbot-arduino-firmware"} commit={firmwareCommit} />
repo={FarmBotRepo.FarmBotArduinoFirmware} commit={firmwareCommit} />
<p><b>{t("Firmware code")}: </b>{firmware_version}</p>
{isNumber(uptime) && <UptimeDisplay uptime_sec={uptime} />}
{isNumber(memory_usage) &&

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 {
@ -53,7 +52,6 @@ export interface PowerAndResetProps {
controlPanelState: ControlPanelState;
dispatch: Function;
sourceFbosConfig: SourceFbosConfig;
shouldDisplay: ShouldDisplay;
botOnline: boolean;
}

View File

@ -4,23 +4,20 @@ import { Collapse, Popover, Position } from "@blueprintjs/core";
import { FactoryResetRow } from "./factory_reset_row";
import { PowerAndResetProps } from "./interfaces";
import { ChangeOwnershipForm } from "./change_ownership_form";
import { Feature } from "../../interfaces";
import { FbosButtonRow } from "./fbos_button_row";
import { Content } from "../../../constants";
import { reboot, powerOff, restartFirmware } from "../../actions";
import { t } from "../../../i18next_wrapper";
export function PowerAndReset(props: PowerAndResetProps) {
const { dispatch, sourceFbosConfig, shouldDisplay, botOnline } = props;
const { dispatch, sourceFbosConfig, botOnline } = props;
const { power_and_reset } = props.controlPanelState;
return <section>
<div style={{ fontSize: "1px" }}>
<Header
expanded={power_and_reset}
title={t("Power and Reset")}
name={"power_and_reset"}
dispatch={dispatch} />
</div>
<Header
expanded={power_and_reset}
title={t("Power and Reset")}
name={"power_and_reset"}
dispatch={dispatch} />
<Collapse isOpen={!!power_and_reset}>
<FbosButtonRow
botOnline={botOnline}
@ -36,14 +33,13 @@ export function PowerAndReset(props: PowerAndResetProps) {
buttonText={t("SHUTDOWN")}
color={"red"}
action={powerOff} />
{shouldDisplay(Feature.firmware_restart) &&
<FbosButtonRow
botOnline={botOnline}
label={t("RESTART FIRMWARE")}
description={Content.RESTART_FIRMWARE}
buttonText={t("RESTART")}
color={"yellow"}
action={restartFirmware} />}
<FbosButtonRow
botOnline={botOnline}
label={t("RESTART FIRMWARE")}
description={Content.RESTART_FIRMWARE}
buttonText={t("RESTART")}
color={"yellow"}
action={restartFirmware} />
<FactoryResetRow
dispatch={dispatch}
sourceFbosConfig={sourceFbosConfig}

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,11 +2,12 @@ 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";
import { EncodersAndEndStops } from "./hardware_settings/encoders_and_endstops";
import { Encoders } from "./hardware_settings/encoders";
import { EndStops } from "./hardware_settings/endstops";
import { Motors } from "./hardware_settings/motors";
import { SpacePanelHeader } from "./hardware_settings/space_panel_header";
import {
@ -15,6 +16,8 @@ import {
import { Popover, Position } from "@blueprintjs/core";
import { FwParamExportMenu } from "./hardware_settings/export_menu";
import { t } from "../../i18next_wrapper";
import { PinBindings } from "./hardware_settings/pin_bindings";
import { ErrorHandling } from "./hardware_settings/error_handling";
export class HardwareSettings extends
React.Component<HardwareSettingsProps, {}> {
@ -27,15 +30,9 @@ export class HardwareSettings extends
const { informational_settings } = this.props.bot.hardware;
const { sync_status } = informational_settings;
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"}
@ -47,46 +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
dispatch={dispatch}
bot={bot}
sourceFwConfig={sourceFwConfig}
firmwareConfig={firmwareConfig}
botDisconnected={botDisconnected} />
<Motors
dispatch={dispatch}
controlPanelState={controlPanelState}
sourceFwConfig={sourceFwConfig}
firmwareHardware={firmwareHardware} />
<EncodersAndEndStops
dispatch={dispatch}
shouldDisplay={this.props.shouldDisplay}
controlPanelState={controlPanelState}
sourceFwConfig={sourceFwConfig}
firmwareHardware={firmwareHardware} />
<PinGuard
dispatch={dispatch}
resources={resources}
controlPanelState={controlPanelState}
sourceFwConfig={sourceFwConfig} />
<DangerZone
dispatch={dispatch}
controlPanelState={controlPanelState}
onReset={MCUFactoryReset}
botDisconnected={botDisconnected} />
</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

@ -1,22 +1,40 @@
const mockDevice = {
calibrate: jest.fn(() => Promise.resolve({}))
};
jest.mock("../../../../device", () => ({
getDevice: () => (mockDevice)
}));
import * as React from "react";
import { mount } from "enzyme";
import { CalibrationRow } from "../calibration_row";
import { bot } from "../../../../__test_support__/fake_state/bot";
import { CalibrationRowProps } from "../../interfaces";
describe("<CalibrationRow />", () => {
const fakeProps = (): CalibrationRowProps => ({
type: "calibrate",
hardware: bot.hardware.mcu_params,
botDisconnected: false,
action: jest.fn(),
toolTip: "calibrate",
title: "calibrate",
axisTitle: "calibrate",
});
describe("<HomingRow />", () => {
it("calls device", () => {
const result = mount(<CalibrationRow
hardware={bot.hardware.mcu_params}
botDisconnected={false} />);
const p = fakeProps();
const result = mount(<CalibrationRow {...p} />);
p.hardware.encoder_enabled_x = 1;
p.hardware.encoder_enabled_y = 1;
p.hardware.encoder_enabled_z = 0;
[0, 1, 2].map(i => result.find("LockableButton").at(i).simulate("click"));
expect(mockDevice.calibrate).toHaveBeenCalledTimes(2);
[{ axis: "y" }, { axis: "x" }].map(x =>
expect(mockDevice.calibrate).toHaveBeenCalledWith(x));
expect(p.action).toHaveBeenCalledTimes(2);
["y", "x"].map(x => expect(p.action).toHaveBeenCalledWith(x));
});
it("is not disabled", () => {
const p = fakeProps();
p.type = "zero";
const result = mount(<CalibrationRow {...p} />);
p.hardware.encoder_enabled_x = 0;
p.hardware.encoder_enabled_y = 1;
p.hardware.encoder_enabled_z = 0;
[0, 1, 2].map(i => result.find("LockableButton").at(i).simulate("click"));
expect(p.action).toHaveBeenCalledTimes(3);
["x", "y", "z"].map(x => expect(p.action).toHaveBeenCalledWith(x));
});
});

View File

@ -1,47 +0,0 @@
import * as React from "react";
import { mount, shallow } from "enzyme";
import { EncodersAndEndStops } from "../encoders_and_endstops";
import { EncodersProps, NumericMCUInputGroupProps } from "../../interfaces";
import { panelState } from "../../../../__test_support__/control_panel_state";
import { bot } from "../../../../__test_support__/fake_state/bot";
import { Dictionary } from "farmbot";
describe("<EncodersAndEndStops />", () => {
const mockFeatures: Dictionary<boolean> = {};
const fakeProps = (): EncodersProps => ({
dispatch: jest.fn(),
controlPanelState: panelState(),
sourceFwConfig: x =>
({ value: bot.hardware.mcu_params[x], consistent: true }),
shouldDisplay: jest.fn(key => mockFeatures[key]),
firmwareHardware: undefined,
});
it("shows encoder labels", () => {
const p = fakeProps();
p.firmwareHardware = undefined;
const wrapper = mount(<EncodersAndEndStops {...p} />);
expect(wrapper.text().toLowerCase()).toContain("encoder");
expect(wrapper.text().toLowerCase()).not.toContain("stall");
});
it("shows stall labels", () => {
const p = fakeProps();
p.firmwareHardware = "express_k10";
const wrapper = mount(<EncodersAndEndStops {...p} />);
expect(wrapper.text().toLowerCase()).not.toContain("encoder");
expect(wrapper.text().toLowerCase()).toContain("stall");
});
it.each<["short" | "long"]>([
["short"],
["long"],
])("uses %s int scaling factor", (size) => {
mockFeatures.long_scaling_factor = size === "short" ? false : true;
const wrapper = shallow(<EncodersAndEndStops {...fakeProps()} />);
const sfProps = wrapper.find("NumericMCUInputGroup").at(2)
.props() as NumericMCUInputGroupProps;
expect(sfProps.name).toEqual("Encoder Scaling");
expect(sfProps.intSize).toEqual(size);
});
});

View File

@ -0,0 +1,32 @@
import * as React from "react";
import { mount } from "enzyme";
import { Encoders } from "../encoders";
import { EncodersProps } from "../../interfaces";
import { panelState } from "../../../../__test_support__/control_panel_state";
import { bot } from "../../../../__test_support__/fake_state/bot";
describe("<Encoders />", () => {
const fakeProps = (): EncodersProps => ({
dispatch: jest.fn(),
controlPanelState: panelState(),
sourceFwConfig: x =>
({ value: bot.hardware.mcu_params[x], consistent: true }),
firmwareHardware: undefined,
});
it("shows encoder labels", () => {
const p = fakeProps();
p.firmwareHardware = undefined;
const wrapper = mount(<Encoders {...p} />);
expect(wrapper.text().toLowerCase()).toContain("encoder");
expect(wrapper.text().toLowerCase()).not.toContain("stall");
});
it("shows stall labels", () => {
const p = fakeProps();
p.firmwareHardware = "express_k10";
const wrapper = mount(<Encoders {...p} />);
expect(wrapper.text().toLowerCase()).not.toContain("encoder");
expect(wrapper.text().toLowerCase()).toContain("stall");
});
});

View File

@ -0,0 +1,21 @@
import * as React from "react";
import { mount } from "enzyme";
import { EndStops } from "../endstops";
import { EndStopsProps } from "../../interfaces";
import { panelState } from "../../../../__test_support__/control_panel_state";
import { bot } from "../../../../__test_support__/fake_state/bot";
describe("<EndStops />", () => {
const fakeProps = (): EndStopsProps => ({
dispatch: jest.fn(),
controlPanelState: panelState(),
sourceFwConfig: x =>
({ value: bot.hardware.mcu_params[x], consistent: true }),
});
it("shows endstop labels", () => {
const p = fakeProps();
const wrapper = mount(<EndStops {...p} />);
expect(wrapper.text().toLowerCase()).toContain("endstop");
});
});

View File

@ -0,0 +1,47 @@
jest.mock("../../../../api/crud", () => ({
edit: jest.fn(),
save: jest.fn(),
}));
import * as React from "react";
import { mount } from "enzyme";
import { ErrorHandling } from "../error_handling";
import { ErrorHandlingProps } from "../../interfaces";
import { panelState } from "../../../../__test_support__/control_panel_state";
import { bot } from "../../../../__test_support__/fake_state/bot";
import { edit, save } from "../../../../api/crud";
import { fakeState } from "../../../../__test_support__/fake_state";
import {
fakeFirmwareConfig
} from "../../../../__test_support__/fake_state/resources";
import {
buildResourceIndex
} from "../../../../__test_support__/resource_index_builder";
describe("<ErrorHandling />", () => {
const fakeConfig = fakeFirmwareConfig();
const state = fakeState();
state.resources = buildResourceIndex([fakeConfig]);
const fakeProps = (): ErrorHandlingProps => ({
dispatch: jest.fn(x => x(jest.fn(), () => state)),
controlPanelState: panelState(),
sourceFwConfig: x =>
({ value: bot.hardware.mcu_params[x], consistent: true }),
});
it("shows error handling labels", () => {
const p = fakeProps();
const wrapper = mount(<ErrorHandling {...p} />);
expect(wrapper.text().toLowerCase()).toContain("error handling");
});
it("toggles retries e-stop parameter", () => {
const p = fakeProps();
p.controlPanelState.error_handling = true;
p.sourceFwConfig = () => ({ value: 1, consistent: true });
const wrapper = mount(<ErrorHandling {...p} />);
wrapper.find("button").at(0).simulate("click");
expect(edit).toHaveBeenCalledWith(fakeConfig, { param_e_stop_on_mov_err: 0 });
expect(save).toHaveBeenCalledWith(fakeConfig.uuid);
});
});

View File

@ -1,7 +1,17 @@
jest.mock("../../../actions", () => ({ updateMCU: jest.fn() }));
jest.mock("../../../actions", () => ({
updateMCU: jest.fn(),
commandErr: jest.fn(),
}));
const mockDevice = {
calibrate: jest.fn(() => Promise.resolve({})),
findHome: jest.fn(() => Promise.resolve({})),
setZero: jest.fn(() => Promise.resolve({})),
};
jest.mock("../../../../device", () => ({ getDevice: () => mockDevice }));
import * as React from "react";
import { mount } from "enzyme";
import { mount, shallow } from "enzyme";
import { HomingAndCalibration } from "../homing_and_calibration";
import { bot } from "../../../../__test_support__/fake_state/bot";
import { updateMCU } from "../../../actions";
@ -10,20 +20,28 @@ import {
} from "../../../../__test_support__/fake_state/resources";
import { error, warning } from "../../../../toast/toast";
import { inputEvent } from "../../../../__test_support__/fake_html_events";
import { panelState } from "../../../../__test_support__/control_panel_state";
import { HomingAndCalibrationProps } from "../../interfaces";
import { CalibrationRow } from "../calibration_row";
describe("<HomingAndCalibration />", () => {
const fakeProps = (): HomingAndCalibrationProps => ({
dispatch: jest.fn(),
bot,
controlPanelState: panelState(),
sourceFwConfig: x => ({
value: bot.hardware.mcu_params[x], consistent: true
}),
firmwareConfig: fakeFirmwareConfig().body,
botDisconnected: false,
firmwareHardware: undefined,
});
function testAxisLengthInput(
provided: string, expected: string | undefined) {
const dispatch = jest.fn();
bot.controlPanelState.homing_and_calibration = true;
const result = mount(<HomingAndCalibration
dispatch={dispatch}
bot={bot}
firmwareConfig={fakeFirmwareConfig().body}
sourceFwConfig={x => ({
value: bot.hardware.mcu_params[x], consistent: true
})}
botDisconnected={false} />);
const p = fakeProps();
p.bot.controlPanelState.homing_and_calibration = true;
const result = mount(<HomingAndCalibration {...p} />);
const e = inputEvent(provided);
const input = result.find("input").first().props();
input.onChange && input.onChange(e);
@ -45,4 +63,33 @@ describe("<HomingAndCalibration />", () => {
expect(warning).not.toHaveBeenCalled();
expect(error).not.toHaveBeenCalled();
});
it("finds home", () => {
const wrapper = shallow(<HomingAndCalibration {...fakeProps()} />);
wrapper.find(CalibrationRow).first().props().action("x");
expect(mockDevice.findHome).toHaveBeenCalledWith({
axis: "x", speed: 100
});
});
it("calibrates", () => {
const wrapper = shallow(<HomingAndCalibration {...fakeProps()} />);
wrapper.find(CalibrationRow).at(1).props().action("all");
expect(mockDevice.calibrate).toHaveBeenCalledWith({ axis: "all" });
});
it("sets zero", () => {
const wrapper = shallow(<HomingAndCalibration {...fakeProps()} />);
wrapper.find(CalibrationRow).last().props().action("all");
expect(mockDevice.setZero).toHaveBeenCalledWith("all");
});
it("shows express board related labels", () => {
const p = fakeProps();
p.firmwareHardware = "express_k10";
p.controlPanelState.homing_and_calibration = true;
const wrapper = shallow(<HomingAndCalibration {...p} />);
expect(wrapper.find(CalibrationRow).first().props().toolTip)
.toContain("stall detection");
});
});

View File

@ -1,33 +0,0 @@
const mockDevice = {
findHome: jest.fn(() => Promise.resolve({}))
};
jest.mock("../../../../device", () => ({
getDevice: () => (mockDevice)
}));
import * as React from "react";
import { mount } from "enzyme";
import { HomingRow } from "../homing_row";
import { bot } from "../../../../__test_support__/fake_state/bot";
describe("<HomingRow />", () => {
it("renders three buttons", () => {
const wrapper = mount(<HomingRow
hardware={bot.hardware.mcu_params}
botDisconnected={false} />);
const txt = wrapper.text().toUpperCase();
["X", "Y", "Z"].map(function (axis) {
expect(txt).toContain(`HOME ${axis}`);
});
});
it("calls device", () => {
const result = mount(<HomingRow
hardware={bot.hardware.mcu_params}
botDisconnected={false} />);
[0, 1, 2].map(i =>
result.find("LockableButton").at(i).simulate("click"));
[{ axis: "x", speed: 100 }, { axis: "y", speed: 100 }].map(x =>
expect(mockDevice.findHome).toHaveBeenCalledWith(x));
});
});

View File

@ -37,8 +37,6 @@ describe("<Motors/>", () => {
it("renders the base case", () => {
const wrapper = render(<Motors {...fakeProps()} />);
["Enable 2nd X Motor",
"Max Retries",
"E-Stop on Movement Error",
"Max Speed (mm/s)"
].map(string =>
expect(wrapper.text().toLowerCase()).toContain(string.toLowerCase()));
@ -48,16 +46,14 @@ describe("<Motors/>", () => {
const p = fakeProps();
p.firmwareHardware = "express_k10";
const wrapper = render(<Motors {...p} />);
expect(wrapper.text()).toContain("Stall");
expect(wrapper.text()).toContain("Current");
expect(wrapper.text()).toContain("Motor Current");
});
it("doesn't show TMC parameters", () => {
const p = fakeProps();
p.firmwareHardware = "farmduino";
const wrapper = render(<Motors {...p} />);
expect(wrapper.text()).not.toContain("Stall");
expect(wrapper.text()).not.toContain("Current");
expect(wrapper.text()).not.toContain("Motor Current");
});
const testParamToggle = (
@ -72,15 +68,6 @@ describe("<Motors/>", () => {
expect(save).toHaveBeenCalledWith(fakeConfig.uuid);
});
};
testParamToggle("toggles retries e-stop parameter", "param_e_stop_on_mov_err", 0);
testParamToggle("toggles enable X2", "movement_secondary_motor_x", 7);
testParamToggle("toggles invert X2", "movement_secondary_motor_invert_x", 8);
it("renders TMC params", () => {
const p = fakeProps();
p.firmwareHardware = "express_k10";
const wrapper = render(<Motors {...p} />);
expect(wrapper.text()).toContain("Motor Current");
expect(wrapper.text()).toContain("Stall Sensitivity");
});
testParamToggle("toggles enable X2", "movement_secondary_motor_x", 6);
testParamToggle("toggles invert X2", "movement_secondary_motor_invert_x", 7);
});

View File

@ -0,0 +1,22 @@
import * as React from "react";
import { mount } from "enzyme";
import { PinBindings } from "../pin_bindings";
import { PinBindingsProps } from "../../interfaces";
import { panelState } from "../../../../__test_support__/control_panel_state";
import {
buildResourceIndex
} from "../../../../__test_support__/resource_index_builder";
describe("<PinBindings />", () => {
const fakeProps = (): PinBindingsProps => ({
dispatch: jest.fn(),
controlPanelState: panelState(),
resources: buildResourceIndex([]).index,
});
it("shows pin binding labels", () => {
const p = fakeProps();
const wrapper = mount(<PinBindings {...p} />);
expect(wrapper.text().toLowerCase()).toContain("pin bindings");
});
});

View File

@ -1,19 +0,0 @@
const mockDevice = {
setZero: jest.fn(() => Promise.resolve())
};
jest.mock("../../../../device", () => ({
getDevice: () => (mockDevice)
}));
import * as React from "react";
import { mount } from "enzyme";
import { ZeroRow } from "../zero_row";
describe("<HomingRow />", () => {
it("calls device", () => {
const result = mount(<ZeroRow botDisconnected={false} />);
[0, 1, 2].map(i => result.find("ZeroButton").at(i).simulate("click"));
["x", "y", "z"].map(x =>
expect(mockDevice.setZero).toHaveBeenCalledWith(x));
expect(mockDevice.setZero).toHaveBeenCalledTimes(3);
});
});

View File

@ -1,19 +1,11 @@
import * as React from "react";
import { getDevice } from "../../../device";
import { Axis } from "../../interfaces";
import { LockableButton } from "../lockable_button";
import { axisTrackingStatus } from "../axis_tracking_status";
import { ToolTips } from "../../../constants";
import { Row, Col, Help } from "../../../ui/index";
import { CalibrationRowProps } from "../interfaces";
import { commandErr } from "../../actions";
import { t } from "../../../i18next_wrapper";
import { Position } from "@blueprintjs/core";
const calibrate = (axis: Axis) => getDevice()
.calibrate({ axis })
.catch(commandErr("Calibration"));
export function CalibrationRow(props: CalibrationRowProps) {
const { hardware, botDisconnected } = props;
@ -21,18 +13,20 @@ export function CalibrationRow(props: CalibrationRowProps) {
return <Row>
<Col xs={6} className={"widget-body-tooltips"}>
<label>
{t("CALIBRATION")}
{t(props.title)}
</label>
<Help text={ToolTips.CALIBRATION} requireClick={true} position={Position.RIGHT} />
<Help text={t(props.toolTip)}
requireClick={true} position={Position.RIGHT} />
</Col>
{axisTrackingStatus(hardware)
.map(row => {
const { axis, disabled } = row;
const { axis } = row;
const hardwareDisabled = props.type == "zero" ? false : row.disabled;
return <Col xs={2} key={axis} className={"centered-button-div"}>
<LockableButton
disabled={disabled || botDisconnected}
onClick={() => calibrate(axis)}>
{t("CALIBRATE {{axis}}", { axis })}
disabled={hardwareDisabled || botDisconnected}
onClick={() => props.action(axis)}>
{`${t(props.axisTitle)} ${axis}`}
</LockableButton>
</Col>;
})}

View File

@ -5,41 +5,53 @@ import { NumericMCUInputGroup } from "../numeric_mcu_input_group";
import { EncodersProps } from "../interfaces";
import { Header } from "./header";
import { Collapse } from "@blueprintjs/core";
import { Feature } from "../../interfaces";
import { t } from "../../../i18next_wrapper";
import { isExpressBoard } from "../firmware_hardware_support";
export function EncodersAndEndStops(props: EncodersProps) {
export function Encoders(props: EncodersProps) {
const { encoders_and_endstops } = props.controlPanelState;
const { dispatch, sourceFwConfig, shouldDisplay, firmwareHardware } = props;
const { encoders } = props.controlPanelState;
const { dispatch, sourceFwConfig, firmwareHardware } = props;
const encodersDisabled = {
x: !sourceFwConfig("encoder_enabled_x").value,
y: !sourceFwConfig("encoder_enabled_y").value,
z: !sourceFwConfig("encoder_enabled_z").value
};
const isExpress = isExpressBoard(firmwareHardware);
return <section>
<Header
expanded={encoders_and_endstops}
title={isExpressBoard(firmwareHardware)
? t("Stall Detection and Endstops")
: t("Encoders and Endstops")}
name={"encoders_and_endstops"}
expanded={encoders}
title={isExpress
? t("Stall Detection")
: t("Encoders")}
name={"encoders"}
dispatch={dispatch} />
<Collapse isOpen={!!encoders_and_endstops}>
<Collapse isOpen={!!encoders}>
<BooleanMCUInputGroup
name={isExpressBoard(firmwareHardware)
name={isExpress
? t("Enable Stall Detection")
: t("Enable Encoders")}
tooltip={ToolTips.ENABLE_ENCODERS}
tooltip={isExpress
? ToolTips.ENABLE_STALL_DETECTION
: ToolTips.ENABLE_ENCODERS}
x={"encoder_enabled_x"}
y={"encoder_enabled_y"}
z={"encoder_enabled_z"}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />
{!isExpressBoard(firmwareHardware) &&
{isExpress &&
<NumericMCUInputGroup
name={t("Stall Sensitivity")}
tooltip={ToolTips.STALL_SENSITIVITY}
x={"movement_stall_sensitivity_x"}
y={"movement_stall_sensitivity_y"}
z={"movement_stall_sensitivity_z"}
gray={encodersDisabled}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />}
{!isExpress &&
<BooleanMCUInputGroup
name={t("Use Encoders for Positioning")}
tooltip={ToolTips.ENCODER_POSITIONING}
@ -49,7 +61,7 @@ export function EncodersAndEndStops(props: EncodersProps) {
grayscale={encodersDisabled}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />}
{!isExpressBoard(firmwareHardware) &&
{!isExpress &&
<BooleanMCUInputGroup
name={t("Invert Encoders")}
tooltip={ToolTips.INVERT_ENCODERS}
@ -61,7 +73,9 @@ export function EncodersAndEndStops(props: EncodersProps) {
sourceFwConfig={sourceFwConfig} />}
<NumericMCUInputGroup
name={t("Max Missed Steps")}
tooltip={ToolTips.MAX_MISSED_STEPS}
tooltip={isExpress
? ToolTips.MAX_MISSED_STEPS_STALL_DETECTION
: ToolTips.MAX_MISSED_STEPS_ENCODERS}
x={"encoder_missed_steps_max_x"}
y={"encoder_missed_steps_max_y"}
z={"encoder_missed_steps_max_z"}
@ -70,14 +84,14 @@ export function EncodersAndEndStops(props: EncodersProps) {
dispatch={dispatch} />
<NumericMCUInputGroup
name={t("Missed Step Decay")}
tooltip={ToolTips.ENCODER_MISSED_STEP_DECAY}
tooltip={ToolTips.MISSED_STEP_DECAY}
x={"encoder_missed_steps_decay_x"}
y={"encoder_missed_steps_decay_y"}
z={"encoder_missed_steps_decay_z"}
gray={encodersDisabled}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />
{!isExpressBoard(firmwareHardware) &&
{!isExpress &&
<NumericMCUInputGroup
name={t("Encoder Scaling")}
tooltip={ToolTips.ENCODER_SCALING}
@ -87,44 +101,10 @@ export function EncodersAndEndStops(props: EncodersProps) {
xScale={sourceFwConfig("movement_microsteps_x").value}
yScale={sourceFwConfig("movement_microsteps_y").value}
zScale={sourceFwConfig("movement_microsteps_z").value}
intSize={shouldDisplay(Feature.long_scaling_factor) ? "long" : "short"}
intSize={"long"}
gray={encodersDisabled}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />}
<BooleanMCUInputGroup
name={t("Enable Endstops")}
tooltip={ToolTips.ENABLE_ENDSTOPS}
x={"movement_enable_endpoints_x"}
y={"movement_enable_endpoints_y"}
z={"movement_enable_endpoints_z"}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />
<BooleanMCUInputGroup
name={t("Swap Endstops")}
tooltip={ToolTips.SWAP_ENDPOINTS}
x={"movement_invert_endpoints_x"}
y={"movement_invert_endpoints_y"}
z={"movement_invert_endpoints_z"}
grayscale={{
x: !sourceFwConfig("movement_enable_endpoints_x").value,
y: !sourceFwConfig("movement_enable_endpoints_y").value,
z: !sourceFwConfig("movement_enable_endpoints_z").value
}}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />
<BooleanMCUInputGroup
name={t("Invert Endstops")}
tooltip={ToolTips.INVERT_ENDPOINTS}
x={"movement_invert_2_endpoints_x"}
y={"movement_invert_2_endpoints_y"}
z={"movement_invert_2_endpoints_z"}
grayscale={{
x: !sourceFwConfig("movement_enable_endpoints_x").value,
y: !sourceFwConfig("movement_enable_endpoints_y").value,
z: !sourceFwConfig("movement_enable_endpoints_z").value
}}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />
</Collapse>
</section>;
}

View File

@ -0,0 +1,57 @@
import * as React from "react";
import { BooleanMCUInputGroup } from "../boolean_mcu_input_group";
import { ToolTips } from "../../../constants";
import { EndStopsProps } from "../interfaces";
import { Header } from "./header";
import { Collapse } from "@blueprintjs/core";
import { t } from "../../../i18next_wrapper";
export function EndStops(props: EndStopsProps) {
const { endstops } = props.controlPanelState;
const { dispatch, sourceFwConfig } = props;
return <section>
<Header
expanded={endstops}
title={"Endstops"}
name={"endstops"}
dispatch={dispatch} />
<Collapse isOpen={!!endstops}>
<BooleanMCUInputGroup
name={t("Enable Endstops")}
tooltip={ToolTips.ENABLE_ENDSTOPS}
x={"movement_enable_endpoints_x"}
y={"movement_enable_endpoints_y"}
z={"movement_enable_endpoints_z"}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />
<BooleanMCUInputGroup
name={t("Swap Endstops")}
tooltip={ToolTips.SWAP_ENDPOINTS}
x={"movement_invert_endpoints_x"}
y={"movement_invert_endpoints_y"}
z={"movement_invert_endpoints_z"}
grayscale={{
x: !sourceFwConfig("movement_enable_endpoints_x").value,
y: !sourceFwConfig("movement_enable_endpoints_y").value,
z: !sourceFwConfig("movement_enable_endpoints_z").value
}}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />
<BooleanMCUInputGroup
name={t("Invert Endstops")}
tooltip={ToolTips.INVERT_ENDPOINTS}
x={"movement_invert_2_endpoints_x"}
y={"movement_invert_2_endpoints_y"}
z={"movement_invert_2_endpoints_z"}
grayscale={{
x: !sourceFwConfig("movement_enable_endpoints_x").value,
y: !sourceFwConfig("movement_enable_endpoints_y").value,
z: !sourceFwConfig("movement_enable_endpoints_z").value
}}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />
</Collapse>
</section>;
}

View File

@ -0,0 +1,53 @@
import * as React from "react";
import { NumericMCUInputGroup } from "../numeric_mcu_input_group";
import { ToolTips } from "../../../constants";
import { ErrorHandlingProps } from "../interfaces";
import { Header } from "./header";
import { Collapse } from "@blueprintjs/core";
import { t } from "../../../i18next_wrapper";
import { McuInputBox } from "../mcu_input_box";
import { settingToggle } from "../../actions";
import { SingleSettingRow } from "./single_setting_row";
import { ToggleButton } from "../../../controls/toggle_button";
export function ErrorHandling(props: ErrorHandlingProps) {
const { error_handling } = props.controlPanelState;
const { dispatch, sourceFwConfig } = props;
const eStopOnMoveError = sourceFwConfig("param_e_stop_on_mov_err");
return <section>
<Header
expanded={error_handling}
title={"Error Handling"}
name={"error_handling"}
dispatch={dispatch} />
<Collapse isOpen={!!error_handling}>
<NumericMCUInputGroup
name={t("Timeout after (seconds)")}
tooltip={ToolTips.TIMEOUT_AFTER}
x={"movement_timeout_x"}
y={"movement_timeout_y"}
z={"movement_timeout_z"}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />
<SingleSettingRow settingType="input"
label={t("Max Retries")}
tooltip={ToolTips.MAX_MOVEMENT_RETRIES}>
<McuInputBox
setting="param_mov_nr_retry"
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />
</SingleSettingRow>
<SingleSettingRow settingType="button"
label={t("E-Stop on Movement Error")}
tooltip={ToolTips.E_STOP_ON_MOV_ERR}>
<ToggleButton
toggleValue={eStopOnMoveError.value}
dim={!eStopOnMoveError.consistent}
toggleAction={() => dispatch(
settingToggle("param_e_stop_on_mov_err", sourceFwConfig))} />
</SingleSettingRow>
</Collapse>
</section>;
}

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

@ -2,19 +2,23 @@ import * as React from "react";
import { BooleanMCUInputGroup } from "../boolean_mcu_input_group";
import { ToolTips } from "../../../constants";
import { NumericMCUInputGroup } from "../numeric_mcu_input_group";
import { HomingRow } from "./homing_row";
import { CalibrationRow } from "./calibration_row";
import { ZeroRow } from "./zero_row";
import { disabledAxisMap } from "../axis_tracking_status";
import { HomingAndCalibrationProps } from "../interfaces";
import { Header } from "./header";
import { Collapse } from "@blueprintjs/core";
import { t } from "../../../i18next_wrapper";
import { calculateScale } from "./motors";
import { isExpressBoard } from "../firmware_hardware_support";
import { getDevice } from "../../../device";
import { commandErr } from "../../actions";
import { CONFIG_DEFAULTS } from "farmbot/dist/config";
export function HomingAndCalibration(props: HomingAndCalibrationProps) {
const { dispatch, bot, sourceFwConfig, firmwareConfig, botDisconnected
const {
dispatch, bot, sourceFwConfig, firmwareConfig, botDisconnected,
firmwareHardware
} = props;
const hardware = firmwareConfig ? firmwareConfig : bot.hardware.mcu_params;
const { homing_and_calibration } = props.bot.controlPanelState;
@ -34,12 +38,43 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
dispatch={dispatch}
expanded={homing_and_calibration} />
<Collapse isOpen={!!homing_and_calibration}>
<HomingRow hardware={hardware} botDisconnected={botDisconnected} />
<CalibrationRow hardware={hardware} botDisconnected={botDisconnected} />
<ZeroRow botDisconnected={botDisconnected} />
<CalibrationRow
type={"find_home"}
title={t("HOMING")}
axisTitle={t("FIND HOME")}
toolTip={isExpressBoard(firmwareHardware)
? ToolTips.HOMING_STALL_DETECTION
: ToolTips.HOMING_ENCODERS}
action={axis => getDevice()
.findHome({ speed: CONFIG_DEFAULTS.speed, axis })
.catch(commandErr("'Find Home' request"))}
hardware={hardware}
botDisconnected={botDisconnected} />
<CalibrationRow
type={"calibrate"}
title={t("CALIBRATION")}
axisTitle={t("CALIBRATE")}
toolTip={isExpressBoard(firmwareHardware)
? ToolTips.CALIBRATION_STALL_DETECTION
: ToolTips.CALIBRATION_ENCODERS}
action={axis => getDevice().calibrate({ axis })
.catch(commandErr("Calibration"))}
hardware={hardware}
botDisconnected={botDisconnected} />
<CalibrationRow
type={"zero"}
title={t("SET ZERO POSITION")}
axisTitle={t("ZERO")}
toolTip={ToolTips.SET_ZERO_POSITION}
action={axis => getDevice().setZero(axis)
.catch(commandErr("Zeroing"))}
hardware={hardware}
botDisconnected={botDisconnected} />
<BooleanMCUInputGroup
name={t("Find Home on Boot")}
tooltip={ToolTips.FIND_HOME_ON_BOOT}
tooltip={isExpressBoard(firmwareHardware)
? ToolTips.FIND_HOME_ON_BOOT_STALL_DETECTION
: ToolTips.FIND_HOME_ON_BOOT_ENCODERS}
disable={disabled}
x={"movement_home_at_boot_x"}
y={"movement_home_at_boot_y"}
@ -88,14 +123,6 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
sourceFwConfig={sourceFwConfig}
dispatch={dispatch}
intSize={"long"} />
<NumericMCUInputGroup
name={t("Timeout after (seconds)")}
tooltip={ToolTips.TIMEOUT_AFTER}
x={"movement_timeout_x"}
y={"movement_timeout_y"}
z={"movement_timeout_z"}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />
</Collapse>
</section>;
}

View File

@ -1,41 +0,0 @@
import * as React from "react";
import { HomingRowProps } from "../interfaces";
import { LockableButton } from "../lockable_button";
import { axisTrackingStatus } from "../axis_tracking_status";
import { ToolTips } from "../../../constants";
import { Row, Col, Help } from "../../../ui/index";
import { CONFIG_DEFAULTS } from "farmbot/dist/config";
import { commandErr } from "../../actions";
import { Axis } from "../../interfaces";
import { getDevice } from "../../../device";
import { t } from "../../../i18next_wrapper";
import { Position } from "@blueprintjs/core";
const speed = CONFIG_DEFAULTS.speed;
const findHome = (axis: Axis) => getDevice()
.findHome({ speed, axis })
.catch(commandErr("'Find Home' request"));
export function HomingRow(props: HomingRowProps) {
const { hardware, botDisconnected } = props;
return <Row>
<Col xs={6} className={"widget-body-tooltips"}>
<label>
{t("HOMING")}
</label>
<Help text={ToolTips.HOMING} requireClick={true} position={Position.RIGHT}/>
</Col>
{axisTrackingStatus(hardware)
.map((row) => {
const { axis, disabled } = row;
return <Col xs={2} key={axis} className={"centered-button-div"}>
<LockableButton
disabled={disabled || botDisconnected}
onClick={() => findHome(axis)}>
{t("FIND HOME {{axis}}", { axis })}
</LockableButton>
</Col>;
})}
</Row>;
}

View File

@ -5,32 +5,14 @@ import { ToggleButton } from "../../../controls/toggle_button";
import { settingToggle } from "../../actions";
import { NumericMCUInputGroup } from "../numeric_mcu_input_group";
import { MotorsProps } from "../interfaces";
import { Row, Col, Help } from "../../../ui/index";
import { Header } from "./header";
import { Collapse, Position } from "@blueprintjs/core";
import { McuInputBox } from "../mcu_input_box";
import { Collapse } from "@blueprintjs/core";
import { t } from "../../../i18next_wrapper";
import { Xyz, McuParamName } from "farmbot";
import { SourceFwConfig } from "../../interfaces";
import { calcMicrostepsPerMm } from "../../../controls/move/direction_axes_props";
import { isTMCBoard, isExpressBoard } from "../firmware_hardware_support";
const SingleSettingRow =
({ label, tooltip, settingType, children }: {
label: string,
tooltip: string,
children: React.ReactChild,
settingType: "button" | "input",
}) =>
<Row>
<Col xs={6} className={"widget-body-tooltips"}>
<label>{label}</label>
<Help text={tooltip} requireClick={true} position={Position.RIGHT} />
</Col>
{settingType === "button"
? <Col xs={2} className={"centered-button-div"}>{children}</Col>
: <Col xs={6}>{children}</Col>}
</Row>;
import { isTMCBoard } from "../firmware_hardware_support";
import { SingleSettingRow } from "./single_setting_row";
export const calculateScale =
(sourceFwConfig: SourceFwConfig): Record<Xyz, number | undefined> => {
@ -51,13 +33,8 @@ export function Motors(props: MotorsProps) {
} = props;
const enable2ndXMotor = sourceFwConfig("movement_secondary_motor_x");
const invert2ndXMotor = sourceFwConfig("movement_secondary_motor_invert_x");
const eStopOnMoveError = sourceFwConfig("param_e_stop_on_mov_err");
const scale = calculateScale(sourceFwConfig);
const encodersDisabled = {
x: !sourceFwConfig("encoder_enabled_x").value,
y: !sourceFwConfig("encoder_enabled_y").value,
z: !sourceFwConfig("encoder_enabled_z").value,
};
return <section>
<Header
expanded={controlPanelState.motors}
@ -65,23 +42,6 @@ export function Motors(props: MotorsProps) {
name={"motors"}
dispatch={dispatch} />
<Collapse isOpen={!!controlPanelState.motors}>
<SingleSettingRow settingType="input"
label={t("Max Retries")}
tooltip={ToolTips.MAX_MOVEMENT_RETRIES}>
<McuInputBox
setting="param_mov_nr_retry"
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />
</SingleSettingRow>
<SingleSettingRow settingType="button"
label={t("E-Stop on Movement Error")}
tooltip={ToolTips.E_STOP_ON_MOV_ERR}>
<ToggleButton
toggleValue={eStopOnMoveError.value}
dim={!eStopOnMoveError.consistent}
toggleAction={() => dispatch(
settingToggle("param_e_stop_on_mov_err", sourceFwConfig))} />
</SingleSettingRow>
<NumericMCUInputGroup
name={t("Max Speed (mm/s)")}
tooltip={ToolTips.MAX_SPEED}
@ -171,16 +131,6 @@ export function Motors(props: MotorsProps) {
z={"movement_motor_current_z"}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />}
{isExpressBoard(firmwareHardware) &&
<NumericMCUInputGroup
name={t("Stall Sensitivity")}
tooltip={ToolTips.STALL_SENSITIVITY}
x={"movement_stall_sensitivity_x"}
y={"movement_stall_sensitivity_y"}
z={"movement_stall_sensitivity_z"}
gray={encodersDisabled}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />}
<SingleSettingRow settingType="button"
label={t("Enable 2nd X Motor")}
tooltip={ToolTips.ENABLE_X2_MOTOR}>

View File

@ -0,0 +1,22 @@
import * as React from "react";
import { PinBindingsProps } from "../interfaces";
import { Header } from "./header";
import { Collapse } from "@blueprintjs/core";
import { PinBindingsContent } from "../../pin_bindings/pin_bindings";
export function PinBindings(props: PinBindingsProps) {
const { pin_bindings } = props.controlPanelState;
const { dispatch, resources } = props;
return <section>
<Header
expanded={pin_bindings}
title={"Pin Bindings"}
name={"pin_bindings"}
dispatch={dispatch} />
<Collapse isOpen={!!pin_bindings}>
<PinBindingsContent dispatch={dispatch} resources={resources} />
</Collapse>
</section>;
}

View File

@ -0,0 +1,20 @@
import * as React from "react";
import { Row, Col, Help } from "../../../ui/index";
import { Position } from "@blueprintjs/core";
export const SingleSettingRow =
({ label, tooltip, settingType, children }: {
label: string,
tooltip: string,
children: React.ReactChild,
settingType: "button" | "input",
}) =>
<Row>
<Col xs={6} className={"widget-body-tooltips"}>
<label>{label}</label>
<Help text={tooltip} requireClick={true} position={Position.RIGHT} />
</Col>
{settingType === "button"
? <Col xs={2} className={"centered-button-div"}>{children}</Col>
: <Col xs={6}>{children}</Col>}
</Row>;

View File

@ -1,40 +0,0 @@
import * as React from "react";
import { getDevice } from "../../../device";
import { Axis } from "../../interfaces";
import { ToolTips } from "../../../constants";
import { Row, Col, Help } from "../../../ui/index";
import { ZeroRowProps } from "../interfaces";
import { commandErr } from "../../actions";
import { t } from "../../../i18next_wrapper";
import { Position } from "@blueprintjs/core";
const zero =
(axis: Axis) => getDevice().setZero(axis).catch(commandErr("Zeroing"));
const AXES: Axis[] = ["x", "y", "z"];
export function ZeroButton(props: { axis: Axis; disabled: boolean; }) {
const { axis, disabled } = props;
return <button
className="fb-button yellow"
disabled={disabled}
onClick={() => zero(axis)}>
{t("zero {{axis}}", { axis })}
</button>;
}
export function ZeroRow({ botDisconnected }: ZeroRowProps) {
return <Row>
<Col xs={6} className={"widget-body-tooltips"}>
<label>
{t("SET ZERO POSITION")}
</label>
<Help text={ToolTips.SET_ZERO_POSITION} requireClick={true}
position={Position.RIGHT} />
</Col>
{AXES.map((axis) => {
return <Col xs={2} key={axis} className={"centered-button-div"}>
<ZeroButton axis={axis} disabled={botDisconnected} />
</Col>;
})}
</Row>;
}

View File

@ -1,17 +1,12 @@
import {
BotState, Xyz, SourceFwConfig,
ControlPanelState, ShouldDisplay
ControlPanelState, Axis
} from "../interfaces";
import { McuParamName, McuParams, FirmwareHardware } from "farmbot/dist";
import { IntegerSize } from "../../util";
import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware";
import { ResourceIndex } from "../../resources/interfaces";
export interface HomingRowProps {
hardware: McuParams;
botDisconnected: boolean;
}
export interface ZeroRowProps {
botDisconnected: boolean;
}
@ -19,9 +14,11 @@ export interface ZeroRowProps {
export interface HomingAndCalibrationProps {
dispatch: Function;
bot: BotState;
controlPanelState: ControlPanelState;
sourceFwConfig: SourceFwConfig;
firmwareConfig: FirmwareConfig | undefined;
botDisconnected: boolean;
firmwareHardware: FirmwareHardware | undefined;
}
export interface BooleanMCUInputGroupProps {
@ -39,8 +36,13 @@ export interface BooleanMCUInputGroupProps {
}
export interface CalibrationRowProps {
type: "find_home" | "calibrate" | "zero";
hardware: McuParams;
botDisconnected: boolean;
action(axis: Axis): void;
toolTip: string;
title: string;
axisTitle: string;
}
export interface NumericMCUInputGroupProps {
@ -85,12 +87,29 @@ export interface MotorsProps {
export interface EncodersProps {
dispatch: Function;
shouldDisplay: ShouldDisplay;
controlPanelState: ControlPanelState;
sourceFwConfig: SourceFwConfig;
firmwareHardware: FirmwareHardware | undefined;
}
export interface EndStopsProps {
dispatch: Function;
controlPanelState: ControlPanelState;
sourceFwConfig: SourceFwConfig;
}
export interface ErrorHandlingProps {
dispatch: Function;
controlPanelState: ControlPanelState;
sourceFwConfig: SourceFwConfig;
}
export interface PinBindingsProps {
dispatch: Function;
controlPanelState: ControlPanelState;
resources: ResourceIndex;
}
export interface DangerZoneProps {
dispatch: Function;
controlPanelState: ControlPanelState;

View File

@ -56,8 +56,12 @@ const pinNumOrNamedPin =
}
: pin;
const DISABLE_DDI = (): DropDownItem => ({
label: t("None"), value: 0
});
const listItems = (resources: ResourceIndex): DropDownItem[] =>
[...peripheralItems(resources), ...pinDropdowns(n => n)];
[DISABLE_DDI(), ...peripheralItems(resources), ...pinDropdowns(n => n)];
const peripheralItems = (resources: ResourceIndex): DropDownItem[] => {
const list = selectAllSavedPeripherals(resources)

View File

@ -1,5 +1,11 @@
import { Dictionary } from "farmbot";
import { DiagnosticMessages } from "../../constants";
import { docLink } from "../../ui/doc_link";
import { trim } from "../../util/util";
const DiagnosticMessagesWiFiOrConfig =
trim(`${DiagnosticMessages.WIFI_OR_CONFIG}
${docLink("for-it-security-professionals")}`);
// I don't like this at all.
// If anyone has a cleaner solution, I'd love to hear it.
@ -16,13 +22,13 @@ export const TRUTH_TABLE: Readonly<Dictionary<string | undefined>> = {
// 17: No MQTT connections.
[0b10001]: DiagnosticMessages.NO_WS_AVAILABLE,
// 24: Browser is connected to API and MQTT.
[0b11000]: DiagnosticMessages.WIFI_OR_CONFIG,
[0b11000]: DiagnosticMessagesWiFiOrConfig,
// 9: At least the browser is connected to MQTT.
[0b01001]: DiagnosticMessages.WIFI_OR_CONFIG,
[0b01001]: DiagnosticMessagesWiFiOrConfig,
// 8: At least the browser is connected to MQTT.
[0b01000]: DiagnosticMessages.WIFI_OR_CONFIG,
[0b01000]: DiagnosticMessagesWiFiOrConfig,
// 25: Farmbot offline.
[0b11001]: DiagnosticMessages.WIFI_OR_CONFIG,
[0b11001]: DiagnosticMessagesWiFiOrConfig,
// 2: Browser offline. Farmbot last seen by the API recently.
[0b00010]: DiagnosticMessages.NO_WS_AVAILABLE,
// 18: Farmbot last seen by the API recently.

View File

@ -5,7 +5,6 @@ import { FarmbotOsSettings } from "./components/farmbot_os_settings";
import { Page, Col, Row } from "../ui/index";
import { mapStateToProps } from "./state_to_props";
import { Props } from "./interfaces";
import { PinBindings } from "./pin_bindings/pin_bindings";
import { getStatus } from "../connectivity/reducer_support";
import { isFwHardwareValue } from "./components/firmware_hardware_support";
@ -48,9 +47,6 @@ export class RawDevices extends React.Component<Props, {}> {
firmwareHardware={firmwareHardware}
sourceFwConfig={this.props.sourceFwConfig}
firmwareConfig={this.props.firmwareConfig} />
<PinBindings
dispatch={this.props.dispatch}
resources={this.props.resources} />
</Col>
</Row>
</Page>;

View File

@ -93,7 +93,7 @@ export enum Feature {
variables = "variables",
}
/** Object fetched from FEATURE_MIN_VERSIONS_URL. */
/** Object fetched from ExternalUrl.featureMinVersions. */
export type MinOsFeatureLookup = Partial<Record<Feature, string>>;
export interface BotState {
@ -245,7 +245,10 @@ export interface HardwareSettingsProps {
export interface ControlPanelState {
homing_and_calibration: boolean;
motors: boolean;
encoders_and_endstops: boolean;
encoders: boolean;
endstops: boolean;
error_handling: boolean;
pin_bindings: boolean;
danger_zone: boolean;
power_and_reset: boolean;
pin_guard: boolean;

View File

@ -1,4 +1,5 @@
import { sortByNameAndPin, ButtonPin } from "../list_and_label_support";
import { sortByNameAndPin, ButtonPin, getSpecialActionLabel } from "../list_and_label_support";
import { PinBindingSpecialAction } from "farmbot/dist/resources/api_resources";
describe("sortByNameAndPin()", () => {
@ -26,3 +27,11 @@ describe("sortByNameAndPin()", () => {
sortTest(1, 1, Order.equal); // GPIO 1 == GPIO 1
});
});
describe("getSpecialActionLabel()", () => {
it("handles undefined values", () => {
expect(getSpecialActionLabel(undefined)).toEqual("None");
expect(getSpecialActionLabel("wrong" as PinBindingSpecialAction))
.toEqual("");
});
});

View File

@ -1,5 +1,5 @@
import * as React from "react";
import { PinBindings } from "../pin_bindings";
import { PinBindingsContent } from "../pin_bindings";
import { mount } from "enzyme";
import { bot } from "../../../__test_support__/fake_state/bot";
import {
@ -8,15 +8,15 @@ import {
import {
fakeSequence, fakePinBinding
} from "../../../__test_support__/fake_state/resources";
import { PinBindingsProps } from "../interfaces";
import { PinBindingsContentProps } from "../interfaces";
import {
SpecialPinBinding,
PinBindingType,
PinBindingSpecialAction
} from "farmbot/dist/resources/api_resources";
describe("<PinBindings/>", () => {
function fakeProps(): PinBindingsProps {
describe("<PinBindingsContent/>", () => {
function fakeProps(): PinBindingsContentProps {
const fakeSequence1 = fakeSequence();
fakeSequence1.body.id = 1;
fakeSequence1.body.name = "Sequence 1";
@ -51,8 +51,8 @@ describe("<PinBindings/>", () => {
it("renders", () => {
const p = fakeProps();
const wrapper = mount(<PinBindings {...p} />);
["pin bindings", "pin number", "none", "bind", "stock bindings"]
const wrapper = mount(<PinBindingsContent {...p} />);
["pin number", "none", "bind", "stock bindings"]
.map(string => expect(wrapper.text().toLowerCase()).toContain(string));
["26", "action"].map(string =>
expect(wrapper.text().toLowerCase()).toContain(string));

View File

@ -4,7 +4,7 @@ import {
PinBindingSpecialAction
} from "farmbot/dist/resources/api_resources";
export interface PinBindingsProps {
export interface PinBindingsContentProps {
dispatch: Function;
resources: ResourceIndex;
}

View File

@ -32,9 +32,14 @@ export const specialActionLabelLookup: { [x: string]: string } = {
export const specialActionList: DropDownItem[] =
Object.values(PinBindingSpecialAction)
.filter(action => action != PinBindingSpecialAction.dump_info)
.map((action: PinBindingSpecialAction) =>
({ label: specialActionLabelLookup[action], value: action }));
export const getSpecialActionLabel =
(action: PinBindingSpecialAction | undefined) =>
specialActionLabelLookup[action || ""] || "";
/** Pin numbers for standard buttons. */
export enum ButtonPin {
estop = 16,

View File

@ -1,5 +1,5 @@
import * as React from "react";
import { Row, Col, FBSelect, NULL_CHOICE, DropDownItem } from "../../ui";
import { Row, Col, FBSelect, DropDownItem } from "../../ui";
import { PinBindingColWidth } from "./pin_bindings";
import { Popover, Position } from "@blueprintjs/core";
import { RpiGpioDiagram } from "./rpi_gpio_diagram";
@ -13,9 +13,10 @@ import { pinBindingBody } from "./tagged_pin_binding_init";
import { error, warning } from "../../toast/toast";
import {
validGpioPins, sysBindings, generatePinLabel, RpiPinList,
bindingTypeLabelLookup, specialActionLabelLookup, specialActionList,
bindingTypeLabelLookup, specialActionList,
reservedPiGPIO,
bindingTypeList
bindingTypeList,
getSpecialActionLabel
} from "./list_and_label_support";
import { SequenceSelectBox } from "../../sequences/sequence_select_box";
import { ResourceIndex } from "../../resources/interfaces";
@ -119,8 +120,6 @@ export class PinBindingInputGroup
<BindingTypeDropDown
bindingType={bindingType}
setBindingType={this.setBindingType} />
</Col>
<Col xs={PinBindingColWidth.target}>
{bindingType == PinBindingType.special
? <ActionTargetDropDown
specialActionInput={specialActionInput}
@ -152,10 +151,10 @@ export const PinNumberInputGroup = (props: {
const selectedPinNumber = isNumber(pinNumberInput) ? {
label: generatePinLabel(pinNumberInput),
value: "" + pinNumberInput
} : NULL_CHOICE;
} : undefined;
return <Row>
<Col xs={1}>
<Col xs={3}>
<Popover position={Position.TOP}>
<i className="fa fa-th-large" />
<RpiGpioDiagram
@ -181,7 +180,7 @@ export const BindingTypeDropDown = (props: {
setBindingType: (ddi: DropDownItem) => void,
}) => {
const { bindingType, setBindingType } = props;
return <FBSelect
return <FBSelect extraClass={"binding-type-dropdown"}
key={"binding_type_input_" + bindingType}
onChange={setBindingType}
selectedItem={{
@ -213,12 +212,13 @@ export const ActionTargetDropDown = (props: {
const { specialActionInput, setSpecialAction } = props;
const selectedSpecialAction = specialActionInput ? {
label: specialActionLabelLookup[specialActionInput || ""],
label: getSpecialActionLabel(specialActionInput),
value: "" + specialActionInput
} : NULL_CHOICE;
} : undefined;
return <FBSelect
key={"special_action_input_" + specialActionInput}
customNullLabel={t("Select an action")}
onChange={setSpecialAction}
selectedItem={selectedSpecialAction}
list={specialActionList} />;

View File

@ -1,8 +1,8 @@
import * as React from "react";
import { Widget, WidgetBody, WidgetHeader, Row, Col } from "../../ui";
import { Row, Col, Help } from "../../ui";
import { ToolTips } from "../../constants";
import { selectAllPinBindings } from "../../resources/selectors";
import { PinBindingsProps, PinBindingListItems } from "./interfaces";
import { PinBindingsContentProps, PinBindingListItems } from "./interfaces";
import { PinBindingsList } from "./pin_bindings_list";
import { PinBindingInputGroup } from "./pin_binding_input_group";
import {
@ -20,9 +20,8 @@ import { t } from "../../i18next_wrapper";
/** Width of UI columns in Pin Bindings widget. */
export enum PinBindingColWidth {
pin = 4,
type = 3,
target = 4,
button = 1
type = 6,
button = 2
}
/** Use binding type to return a sequence ID or a special action. */
@ -64,34 +63,29 @@ const PinBindingsListHeader = () =>
<label>
{t("Binding")}
</label>
</Col>
<Col xs={PinBindingColWidth.target}>
<label>
{t("target")}
</label>
<Help text={ToolTips.PIN_BINDINGS} />
</Col>
</Row>;
export const PinBindings = (props: PinBindingsProps) => {
export const PinBindingsContent = (props: PinBindingsContentProps) => {
const { dispatch, resources } = props;
const pinBindings = apiPinBindings(resources);
return <Widget className="pin-bindings-widget">
<WidgetHeader
title={t("Pin Bindings")}
helpText={ToolTips.PIN_BINDINGS}>
return <div className="pin-bindings">
<Row>
<StockPinBindingsButton dispatch={dispatch} />
<Popover
position={Position.RIGHT_TOP}
interactionKind={PopoverInteractionKind.HOVER}
portalClassName={"bindings-warning-icon"}
popoverClassName={"help"}>
<i className="fa fa-exclamation-triangle" />
<div>
{t(ToolTips.PIN_BINDING_WARNING)}
</div>
</Popover>
<StockPinBindingsButton dispatch={dispatch} />
</WidgetHeader>
<WidgetBody>
</Row>
<div>
<PinBindingsListHeader />
<PinBindingsList
pinBindings={pinBindings}
@ -101,6 +95,6 @@ export const PinBindings = (props: PinBindingsProps) => {
pinBindings={pinBindings}
dispatch={dispatch}
resources={resources} />
</WidgetBody>
</Widget>;
</div>
</div>;
};

View File

@ -1,7 +1,7 @@
import * as React from "react";
import {
bindingTypeLabelLookup, specialActionLabelLookup,
generatePinLabel, sortByNameAndPin
bindingTypeLabelLookup,
generatePinLabel, sortByNameAndPin, getSpecialActionLabel
} from "./list_and_label_support";
import { destroy } from "../../api/crud";
import { error } from "../../toast/toast";
@ -36,12 +36,10 @@ export const PinBindingsList = (props: PinBindingsListProps) => {
{generatePinLabel(pin_number)}
</Col>
<Col xs={PinBindingColWidth.type}>
{t(bindingTypeLabelLookup[binding_type || ""])}
</Col>
<Col xs={PinBindingColWidth.target}>
{t(bindingTypeLabelLookup[binding_type || ""])}:&nbsp;
{sequence_id
? findSequenceById(resources, sequence_id).body.name
: t(specialActionLabelLookup[special_action || ""])}
: t(getSpecialActionLabel(special_action))}
</Col>
<Col xs={PinBindingColWidth.button}>
<button

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";
@ -27,7 +26,10 @@ export const initialState = (): BotState => ({
controlPanelState: {
homing_and_calibration: false,
motors: false,
encoders_and_endstops: false,
encoders: false,
endstops: false,
error_handling: false,
pin_bindings: false,
danger_zone: false,
power_and_reset: false,
pin_guard: false
@ -116,7 +118,10 @@ export const botReducer = generateReducer<BotState>(initialState())
.add<boolean>(Actions.BULK_TOGGLE_CONTROL_PANEL, (s, a) => {
s.controlPanelState.homing_and_calibration = a.payload;
s.controlPanelState.motors = a.payload;
s.controlPanelState.encoders_and_endstops = a.payload;
s.controlPanelState.encoders = a.payload;
s.controlPanelState.endstops = a.payload;
s.controlPanelState.error_handling = a.payload;
s.controlPanelState.pin_bindings = a.payload;
s.controlPanelState.pin_guard = a.payload;
s.controlPanelState.danger_zone = a.payload;
return s;
@ -199,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

@ -0,0 +1,51 @@
enum Org {
FarmBot = "FarmBot",
FarmBotLabs = "FarmBot-Labs",
}
export enum FarmBotRepo {
FarmBotWebApp = "Farmbot-Web-App",
FarmBotOS = "farmbot_os",
FarmBotArduinoFirmware = "farmbot-arduino-firmware",
}
enum FbosFile {
featureMinVersions = "FEATURE_MIN_VERSIONS.json",
osReleaseNotes = "RELEASE_NOTES.md",
}
export namespace ExternalUrl {
const GITHUB = "https://github.com";
const GITHUB_RAW = "https://raw.githubusercontent.com";
const GITHUB_API = "https://api.github.com";
const OPENFARM = "https://openfarm.cc";
const SOFTWARE_DOCS = "https://software.farm.bot";
const FORUM = "http://forum.farmbot.org";
const SHOPIFY_CDN = "https://cdn.shopify.com/s/files/1/2040/0289/files";
const FBOS_RAW = `${GITHUB_RAW}/${Org.FarmBot}/${FarmBotRepo.FarmBotOS}`;
export const featureMinVersions = `${FBOS_RAW}/${FbosFile.featureMinVersions}`;
export const osReleaseNotes = `${FBOS_RAW}/${FbosFile.osReleaseNotes}`;
export const latestRelease =
`${GITHUB_API}/repos/${Org.FarmBot}/${FarmBotRepo.FarmBotOS}/releases/latest`;
export const gitHubFarmBot = `${GITHUB}/${Org.FarmBot}`;
export const webAppRepo =
`${GITHUB}/${Org.FarmBot}/${FarmBotRepo.FarmBotWebApp}`;
export const softwareDocs = `${SOFTWARE_DOCS}/docs`;
export const softwareForum = `${FORUM}/c/software`;
export namespace OpenFarm {
export const cropApi = `${OPENFARM}/api/v1/crops/`;
export const cropBrowse = `${OPENFARM}/crops/`;
export const newCrop = `${OPENFARM}/en/crops/new`;
}
export namespace Videos {
export const desktop =
`${SHOPIFY_CDN}/Farm_Designer_Loop.mp4?9552037556691879018`;
export const mobile = `${SHOPIFY_CDN}/Controls.png?9668345515035078097`;
}
}

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

@ -67,9 +67,6 @@ export namespace OpenFarm {
type: string;
attributes: ImageAttrs;
}
export const cropUrl = "https://openfarm.cc/api/v1/crops";
export const browsingCropUrl = "https://openfarm.cc/crops/";
}
/** Returned by https://openfarm.cc/api/v1/crops?filter=q */
export interface CropSearchResult {

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

@ -24,6 +24,7 @@ import {
import { startCase, isArray, chain, isNumber } from "lodash";
import { t } from "../../i18next_wrapper";
import { Panel } from "../panel_header";
import { ExternalUrl } from "../../external_urls";
interface InfoFieldProps {
title: string;
@ -170,7 +171,7 @@ const CropDragInfoTile =
const EditOnOpenFarm = ({ slug }: { slug: string }) =>
<div className="edit-on-openfarm">
<span>{t("Edit on")}&nbsp;</span>
<a href={OpenFarm.browsingCropUrl + slug} target="_blank"
<a href={ExternalUrl.OpenFarm.cropBrowse + slug} target="_blank"
title={t("Open OpenFarm.cc in a new tab")}>
{"OpenFarm"}
</a>

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

@ -5,6 +5,7 @@ import {
} from "../../ui/empty_state_wrapper";
import { Content } from "../../constants";
import { t } from "../../i18next_wrapper";
import { ExternalUrl } from "../../external_urls";
/** A stripped down version of OFSearchResult */
interface Result {
@ -24,7 +25,7 @@ export class OpenFarmResults extends React.Component<SearchResultProps, {}> {
get text(): JSX.Element {
return <p>{`${t(Content.CROP_NOT_FOUND_INTRO)} `}
<a href="https://openfarm.cc/en/crops/new" target="_blank">
<a href={ExternalUrl.OpenFarm.newCrop} target="_blank">
{t(Content.CROP_NOT_FOUND_LINK)}
</a>
</p>;

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>;

Some files were not shown because too many files have changed in this diff Show More