Merge pull request #1701 from FarmBot/staging

v9.1.3 - Jolly Juniper
hmm
Rick Carlino 2020-02-20 12:57:33 -06:00 committed by GitHub
commit a5b1d5631e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
188 changed files with 2998 additions and 1746 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

@ -1,8 +1,8 @@
module Tools
class Destroy < Mutations::Command
STILL_IN_USE = "Can't delete tool because the following sequences are "\
"still using it: %s"
STILL_IN_SLOT = "Can't delete tool because it is still in a tool slot. "\
STILL_IN_USE = "Can't delete tool because the following sequences are " \
"still using it: %s"
STILL_IN_SLOT = "Can't delete tool because it is still in a tool slot. " \
"Please remove it from the tool slot first."
required do
@ -15,10 +15,11 @@ module Tools
end
def execute
maybe_unmount_tool
tool.destroy!
end
private
private
def slot
@slot ||= tool.tool_slot
@ -33,8 +34,14 @@ private
end
def names
@names ||= \
@names ||=
InUseTool.where(tool_id: tool.id).pluck(:sequence_name).join(", ")
end
def maybe_unmount_tool
if tool.device.mounted_tool_id == tool.id
tool.device.update!(mounted_tool_id: nil)
end
end
end
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

@ -1,10 +1,9 @@
jest.mock("../util", () => {
return {
attachToRoot: jest.fn(),
// Incidental mock. Can be removed if errors go away.
trim: jest.fn(x => x)
};
});
jest.mock("../util", () => ({
attachToRoot: jest.fn(),
// Incidental mock. Can be removed if errors go away.
trim: jest.fn(x => x),
urlFriendly: jest.fn(),
}));
jest.mock("../redux/store", () => {
return { store: { dispatch: jest.fn() } };

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/staging/FEATURE_MIN_VERSIONS.json");
expect(ExternalUrl.osReleaseNotes)
.toEqual("https://raw.githubusercontent.com/FarmBot/farmbot_os/staging/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("https://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.Video.desktop)
.toEqual("https://cdn.shopify.com/s/files/1/2040/0289/files/Farm_Designer_Loop.mp4?9552037556691879018");
expect(ExternalUrl.Video.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
@ -674,9 +702,9 @@ export namespace Content {
trim(`FarmBot sent a malformed message. You may need to upgrade
FarmBot OS. Please upgrade FarmBot OS and log back in.`);
export const OLD_FBOS_REC_UPGRADE = trim(`Your version of FarmBot OS is
outdated and will soon no longer be supported. Please update your device as
soon as possible.`);
export const OLD_FBOS_REC_UPGRADE =
trim(`Your version of FarmBot OS is outdated and will soon no longer
be supported. Please update your device as soon as possible.`);
export const EXPERIMENTAL_WARNING =
trim(`Warning! This is an EXPERIMENTAL feature. This feature may be
@ -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.`);
@ -784,7 +812,10 @@ export namespace Content {
trim(`add this crop on OpenFarm?`);
export const NO_TOOLS =
trim(`Press "+" to add a new tool.`);
trim(`Press "+" to add a new tool or seed container.`);
export const NO_SEED_CONTAINERS =
trim(`Press "+" to add a seed container.`);
export const MOUNTED_TOOL =
trim(`The tool currently mounted to the UTM can be set here or by using
@ -859,12 +890,23 @@ export namespace TourContent {
selecting one, and dragging it into the garden.`);
export const ADD_TOOLS =
trim(`Press edit and then the + button to add tools and seed containers.`);
trim(`Press the + button to add tools and seed containers.`);
export const ADD_SEED_CONTAINERS =
trim(`Press the + button to add seed containers.`);
export const ADD_TOOLS_AND_SLOTS =
trim(`Press the + button to add tools and seed containers. Then create
tool slots for them to by pressing the tool slot + button.`);
export const ADD_SEED_CONTAINERS_AND_SLOTS =
trim(`Press the + button to add seed containers. Then create
slots for them to by pressing the seed container slot + button.`);
export const ADD_TOOLS_SLOTS =
trim(`Add the newly created tools and seed containers to the
corresponding tool slots on FarmBot:
press edit and then + to create a tool slot.`);
press the + button to create a tool slot.`);
export const ADD_PERIPHERALS =
trim(`Press edit and then the + button to add peripherals.`);
@ -902,6 +944,87 @@ export namespace TourContent {
trim(`Toggle various settings to customize your web app experience.`);
}
export enum DeviceSetting {
// Homing and calibration
homingAndCalibration = `Homing and Calibration`,
homing = `Homing`,
calibration = `Calibration`,
setZeroPosition = `Set Zero Position`,
findHomeOnBoot = `Find Home on Boot`,
stopAtHome = `Stop at Home`,
stopAtMax = `Stop at Max`,
negativeCoordinatesOnly = `Negative Coordinates Only`,
axisLength = `Axis Length (mm)`,
// Motors
motors = `Motors`,
maxSpeed = `Max Speed (mm/s)`,
homingSpeed = `Homing Speed (mm/s)`,
minimumSpeed = `Minimum Speed (mm/s)`,
accelerateFor = `Accelerate for (mm)`,
stepsPerMm = `Steps per MM`,
microstepsPerStep = `Microsteps per step`,
alwaysPowerMotors = `Always Power Motors`,
invertMotors = `Invert Motors`,
motorCurrent = `Motor Current`,
enable2ndXMotor = `Enable 2nd X Motor`,
invert2ndXMotor = `Invert 2nd X Motor`,
// Encoders / Stall Detection
encoders = `Encoders`,
stallDetection = `Stall Detection`,
enableEncoders = `Enable Encoders`,
enableStallDetection = `Enable Stall Detection`,
stallSensitivity = `Stall Sensitivity`,
useEncodersForPositioning = `Use Encoders for Positioning`,
invertEncoders = `Invert Encoders`,
maxMissedSteps = `Max Missed Steps`,
missedStepDecay = `Missed Step Decay`,
encoderScaling = `Encoder Scaling`,
// Endstops
endstops = `Endstops`,
enableEndstops = `Enable Endstops`,
swapEndstops = `Swap Endstops`,
invertEndstops = `Invert Endstops`,
// Error handling
errorHandling = `Error Handling`,
timeoutAfter = `Timeout after (seconds)`,
maxRetries = `Max Retries`,
estopOnMovementError = `E-Stop on Movement Error`,
// Pin Guard
pinGuard = `Pin Guard`,
// Danger Zone
dangerZone = `dangerZone`,
resetHardwareParams = `Reset hardware parameter defaults`,
// Pin Bindings
pinBindings = `Pin Bindings`,
// FarmBot OS
name = `name`,
timezone = `timezone`,
camera = `camera`,
firmware = `firmware`,
farmbotOSAutoUpdate = `Farmbot OS Auto Update`,
farmbotOS = `Farmbot OS`,
autoSync = `Auto Sync`,
bootSequence = `Boot Sequence`,
// Power and Reset
powerAndReset = `Power and Reset`,
restartFarmbot = `Restart Farmbot`,
shutdownFarmbot = `Shutdown Farmbot`,
restartFirmware = `Restart Firmware`,
factoryReset = `Factory Reset`,
autoFactoryReset = `Automatic Factory Reset`,
connectionAttemptPeriod = `Connection Attempt Period`,
changeOwnership = `Change Ownership`,
}
export namespace DiagnosticMessages {
export const OK = trim(`All systems nominal.`);
@ -924,8 +1047,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

@ -38,6 +38,7 @@ export class RawControls extends React.Component<Props, {}> {
getWebAppConfigVal={this.props.getWebAppConfigVal} />
peripherals = () => <Peripherals
firmwareHardware={this.props.firmwareHardware}
bot={this.props.bot}
peripherals={this.props.peripherals}
dispatch={this.props.dispatch}
@ -50,6 +51,7 @@ export class RawControls extends React.Component<Props, {}> {
sensors = () => this.hideSensors
? <div id="hidden-sensors-widget" />
: <Sensors
firmwareHardware={this.props.firmwareHardware}
bot={this.props.bot}
sensors={this.props.sensors}
dispatch={this.props.dispatch}

View File

@ -5,7 +5,7 @@ import { bot } from "../../../__test_support__/fake_state/bot";
import { PeripheralsProps } from "../../../devices/interfaces";
import { fakePeripheral } from "../../../__test_support__/fake_state/resources";
import { clickButton } from "../../../__test_support__/helpers";
import { SpecialStatus } from "farmbot";
import { SpecialStatus, FirmwareHardware } from "farmbot";
import { error } from "../../../toast/toast";
describe("<Peripherals />", () => {
@ -14,7 +14,8 @@ describe("<Peripherals />", () => {
bot,
peripherals: [fakePeripheral()],
dispatch: jest.fn(),
disabled: false
disabled: false,
firmwareHardware: undefined,
};
}
@ -73,11 +74,18 @@ describe("<Peripherals />", () => {
expect(p.dispatch).toHaveBeenCalled();
});
it("adds farmduino peripherals", () => {
it.each<[FirmwareHardware, number]>([
["arduino", 2],
["farmduino", 5],
["farmduino_k14", 5],
["farmduino_k15", 5],
["express_k10", 3],
])("adds peripherals: %s", (firmware, expectedAdds) => {
const p = fakeProps();
p.firmwareHardware = firmware;
const wrapper = mount(<Peripherals {...p} />);
wrapper.setState({ isEditing: true });
clickButton(wrapper, 3, "farmduino");
expect(p.dispatch).toHaveBeenCalledTimes(5);
clickButton(wrapper, 3, "stock");
expect(p.dispatch).toHaveBeenCalledTimes(expectedAdds);
});
});

View File

@ -56,12 +56,31 @@ export class Peripherals
this.props.dispatch(init("Peripheral", { pin, label }));
};
farmduinoPeripherals = () => {
this.newPeripheral(7, t("Lighting"));
this.newPeripheral(8, t("Water"));
this.newPeripheral(9, t("Vacuum"));
this.newPeripheral(10, t("Peripheral ") + "4");
this.newPeripheral(12, t("Peripheral ") + "5");
get stockPeripherals() {
switch (this.props.firmwareHardware) {
case "arduino":
return [
{ pin: 8, label: t("Water") },
{ pin: 9, label: t("Vacuum") },
];
case "farmduino":
case "farmduino_k14":
case "farmduino_k15":
default:
return [
{ pin: 7, label: t("Lighting") },
{ pin: 8, label: t("Water") },
{ pin: 9, label: t("Vacuum") },
{ pin: 10, label: t("Peripheral ") + "4" },
{ pin: 12, label: t("Peripheral ") + "5" },
];
case "express_k10":
return [
{ pin: 7, label: t("Lighting") },
{ pin: 8, label: t("Water") },
{ pin: 9, label: t("Vacuum") },
];
}
}
render() {
@ -92,10 +111,11 @@ export class Peripherals
hidden={!isEditing}
className="fb-button green"
type="button"
onClick={this.farmduinoPeripherals}>
onClick={() => this.stockPeripherals.map(p =>
this.newPeripheral(p.pin, p.label))}>
<i className="fa fa-plus" style={{ marginRight: "0.5rem" }} />
Farmduino
</button>
{t("Stock")}
</button>
</WidgetHeader>
<WidgetBody>
{this.showPins()}

View File

@ -18,7 +18,8 @@ describe("<Sensors />", () => {
bot,
sensors: [fakeSensor1, fakeSensor2],
dispatch: jest.fn(),
disabled: false
disabled: false,
firmwareHardware: undefined,
};
}
@ -68,8 +69,16 @@ describe("<Sensors />", () => {
it("adds stock sensors", () => {
const p = fakeProps();
const wrapper = mount(<Sensors {...p} />);
expect(wrapper.text().toLowerCase()).toContain("stock sensors");
wrapper.setState({ isEditing: true });
clickButton(wrapper, 3, "stock sensors");
expect(p.dispatch).toHaveBeenCalledTimes(2);
});
it("doesn't display + stock button", () => {
const p = fakeProps();
p.firmwareHardware = "express_k10";
const wrapper = mount(<Sensors {...p} />);
expect(wrapper.text().toLowerCase()).not.toContain("stock sensors");
});
});

View File

@ -100,4 +100,11 @@ describe("<SensorList/>", function () {
readSensorBtn.last().simulate("click");
expect(mockDevice.readPin).not.toHaveBeenCalled();
});
it("renders analog reading", () => {
const p = fakeProps();
p.pins[50] && (p.pins[50].value = 600);
const wrapper = mount(<SensorList {...p} />);
expect(wrapper.html()).toContain("margin-left: -3.5rem");
});
});

View File

@ -10,6 +10,7 @@ import { saveAll, init } from "../../api/crud";
import { ToolTips } from "../../constants";
import { uniq } from "lodash";
import { t } from "../../i18next_wrapper";
import { isExpressBoard } from "../../devices/components/firmware_hardware_support";
export class Sensors extends React.Component<SensorsProps, SensorState> {
constructor(props: SensorsProps) {
@ -79,14 +80,15 @@ export class Sensors extends React.Component<SensorsProps, SensorState> {
onClick={() => this.newSensor()}>
<i className="fa fa-plus" />
</button>
<button
hidden={!isEditing}
className="fb-button green"
type="button"
onClick={this.stockSensors}>
<i className="fa fa-plus" style={{ marginRight: "0.5rem" }} />
{t("Stock sensors")}
</button>
{!isExpressBoard(this.props.firmwareHardware) &&
<button
hidden={!isEditing}
className="fb-button green"
type="button"
onClick={this.stockSensors}>
<i className="fa fa-plus" style={{ marginRight: "0.5rem" }} />
{t("Stock sensors")}
</button>}
</WidgetHeader>
<WidgetBody>
{this.showPins()}

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

@ -1,5 +1,6 @@
// Padding for the popups.
.bp3-popover-content {
z-index: 999;
padding: 1rem;
}

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;
}
}
@ -1601,3 +1629,23 @@ textarea:focus {
}
}
}
.section {
display: block !important;
}
.highlight,
.unhighlight {
display: flex;
}
.highlight {
background-color: $light_yellow;
box-shadow: 0px 0px 7px 4px $light_yellow;
}
.unhighlight {
transition: background-color 10s linear, box-shadow 10s linear;
background-color: transparent;
box-shadow: none;
}

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.Video.desktop} type="video/mp4" />
</video>
<img className="demo-phone" src={PHONE_URL} />
<img className="demo-phone" src={ExternalUrl.Video.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

@ -7,13 +7,14 @@ import { ToggleButton } from "../../../controls/toggle_button";
import { settingToggle } from "../../actions";
import { bot } from "../../../__test_support__/fake_state/bot";
import { BooleanMCUInputGroupProps } from "../interfaces";
import { DeviceSetting } from "../../../constants";
describe("BooleanMCUInputGroup", () => {
const fakeProps = (): BooleanMCUInputGroupProps => ({
sourceFwConfig: x => ({ value: bot.hardware.mcu_params[x], consistent: true }),
dispatch: jest.fn(),
tooltip: "Tooltip",
name: "Name",
label: DeviceSetting.invertEncoders,
x: "encoder_invert_x",
y: "encoder_invert_y",
z: "encoder_invert_z",

View File

@ -22,6 +22,8 @@ import axios from "axios";
import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings";
import { edit } from "../../../api/crud";
import { fakeWebAppConfig } from "../../../__test_support__/fake_state/resources";
import { formEvent } from "../../../__test_support__/fake_html_events";
import { Content } from "../../../constants";
describe("<FarmbotOsSettings />", () => {
beforeEach(() => {
@ -54,8 +56,8 @@ describe("<FarmbotOsSettings />", () => {
const osSettings = mount(<FarmbotOsSettings {...fakeProps()} />);
expect(osSettings.find("input").length).toBe(1);
expect(osSettings.find("button").length).toBe(7);
["NAME", "TIME ZONE", "FARMBOT OS", "CAMERA", "FIRMWARE"]
.map(string => expect(osSettings.text()).toContain(string));
["name", "time zone", "farmbot os", "camera", "firmware"]
.map(string => expect(osSettings.text().toLowerCase()).toContain(string));
});
it("fetches OS release notes", async () => {
@ -115,4 +117,18 @@ describe("<FarmbotOsSettings />", () => {
const osSettings = shallow(<FarmbotOsSettings {...p} />);
expect(osSettings.find("BootSequenceSelector").length).toEqual(1);
});
it("prevents default form submit action", () => {
const osSettings = shallow(<FarmbotOsSettings {...fakeProps()} />);
const e = formEvent();
osSettings.find("form").simulate("submit", e);
expect(e.preventDefault).toHaveBeenCalled();
});
it("warns about timezone mismatch", () => {
const p = fakeProps();
p.deviceAccount.body.timezone = "different";
const osSettings = mount(<FarmbotOsSettings {...p} />);
expect(osSettings.text()).toContain(Content.DIFFERENT_TZ_WARNING);
});
});

View File

@ -1,4 +1,5 @@
import { boardType } from "../firmware_hardware_support";
import { boardType, getFwHardwareValue } from "../firmware_hardware_support";
import { fakeFbosConfig } from "../../../__test_support__/fake_state/resources";
describe("boardType()", () => {
it("returns Farmduino", () => {
@ -32,3 +33,18 @@ describe("boardType()", () => {
expect(boardType("none")).toEqual("none");
});
});
describe("getFwHardwareValue()", () => {
it("returns undefined", () => {
const fbosConfig = fakeFbosConfig();
fbosConfig.body.firmware_hardware = "wrong";
expect(getFwHardwareValue(fbosConfig)).toEqual(undefined);
expect(getFwHardwareValue(undefined)).toEqual(undefined);
});
it("returns real value", () => {
const fbosConfig = fakeFbosConfig();
fbosConfig.body.firmware_hardware = "express_k10";
expect(getFwHardwareValue(fbosConfig)).toEqual("express_k10");
});
});

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

@ -0,0 +1,81 @@
jest.mock("../../actions", () => ({
toggleControlPanel: jest.fn(),
}));
import * as React from "react";
import { mount } from "enzyme";
import {
Highlight, HighlightProps, maybeHighlight, maybeOpenPanel, highlight
} from "../maybe_highlight";
import { DeviceSetting } from "../../../constants";
import { panelState } from "../../../__test_support__/control_panel_state";
import { toggleControlPanel } from "../../actions";
describe("<Highlight />", () => {
const fakeProps = (): HighlightProps => ({
settingName: DeviceSetting.motors,
children: <div />,
className: "section",
});
it("fades highlight", () => {
const p = fakeProps();
const wrapper = mount<Highlight>(<Highlight {...p} />);
wrapper.setState({ className: "highlight" });
wrapper.instance().componentDidMount();
expect(wrapper.state().className).toEqual("unhighlight");
});
});
describe("maybeHighlight()", () => {
beforeEach(() => {
highlight.opened = false;
highlight.highlighted = false;
});
it("highlights only once", () => {
location.search = "?highlight=motors";
expect(maybeHighlight(DeviceSetting.motors)).toEqual("highlight");
expect(maybeHighlight(DeviceSetting.motors)).toEqual("");
});
it("doesn't highlight: different setting", () => {
location.search = "?highlight=name";
expect(maybeHighlight(DeviceSetting.motors)).toEqual("");
});
it("doesn't highlight: no matches", () => {
location.search = "?highlight=na";
expect(maybeHighlight(DeviceSetting.motors)).toEqual("");
});
});
describe("maybeOpenPanel()", () => {
beforeEach(() => {
highlight.opened = false;
highlight.highlighted = false;
});
it("opens panel only once", () => {
location.search = "?highlight=motors";
maybeOpenPanel(panelState())(jest.fn());
expect(toggleControlPanel).toHaveBeenCalledWith("motors");
jest.resetAllMocks();
maybeOpenPanel(panelState())(jest.fn());
expect(toggleControlPanel).not.toHaveBeenCalled();
});
it("doesn't open panel: already open", () => {
location.search = "?highlight=motors";
const panels = panelState();
panels.motors = true;
maybeOpenPanel(panels)(jest.fn());
expect(toggleControlPanel).not.toHaveBeenCalled();
});
it("doesn't open panel: no search term", () => {
location.search = "";
maybeOpenPanel(panelState())(jest.fn());
expect(toggleControlPanel).not.toHaveBeenCalled();
});
});

View File

@ -4,12 +4,14 @@ import { settingToggle } from "../actions";
import { Row, Col, Help } from "../../ui/index";
import { BooleanMCUInputGroupProps } from "./interfaces";
import { Position } from "@blueprintjs/core";
import { t } from "../../i18next_wrapper";
import { Highlight } from "./maybe_highlight";
export function BooleanMCUInputGroup(props: BooleanMCUInputGroupProps) {
const {
tooltip,
name,
label,
x,
y,
z,
@ -26,40 +28,42 @@ export function BooleanMCUInputGroup(props: BooleanMCUInputGroupProps) {
const zParam = sourceFwConfig(z);
return <Row>
<Col xs={6} className={"widget-body-tooltips"}>
<label>
{name}
{caution &&
<i className="fa fa-exclamation-triangle caution-icon" />}
</label>
<Help text={tooltip} requireClick={true} position={Position.RIGHT} />
</Col>
<Col xs={2} className={"centered-button-div"}>
<ToggleButton
grayscale={grayscale?.x}
disabled={disable?.x}
dim={!xParam.consistent}
toggleValue={xParam.value}
toggleAction={() =>
dispatch(settingToggle(x, sourceFwConfig, displayAlert))} />
</Col>
<Col xs={2} className={"centered-button-div"}>
<ToggleButton
grayscale={grayscale?.y}
disabled={disable?.y}
dim={!yParam.consistent}
toggleValue={yParam.value}
toggleAction={() =>
dispatch(settingToggle(y, sourceFwConfig, displayAlert))} />
</Col>
<Col xs={2} className={"centered-button-div"}>
<ToggleButton
grayscale={grayscale?.z}
disabled={disable?.z}
dim={!zParam.consistent}
toggleValue={zParam.value}
toggleAction={() =>
dispatch(settingToggle(z, sourceFwConfig, displayAlert))} />
</Col>
<Highlight settingName={label}>
<Col xs={6} className={"widget-body-tooltips"}>
<label>
{t(label)}
{caution &&
<i className="fa fa-exclamation-triangle caution-icon" />}
</label>
<Help text={tooltip} requireClick={true} position={Position.RIGHT} />
</Col>
<Col xs={2} className={"centered-button-div"}>
<ToggleButton
grayscale={grayscale?.x}
disabled={disable?.x}
dim={!xParam.consistent}
toggleValue={xParam.value}
toggleAction={() =>
dispatch(settingToggle(x, sourceFwConfig, displayAlert))} />
</Col>
<Col xs={2} className={"centered-button-div"}>
<ToggleButton
grayscale={grayscale?.y}
disabled={disable?.y}
dim={!yParam.consistent}
toggleValue={yParam.value}
toggleAction={() =>
dispatch(settingToggle(y, sourceFwConfig, displayAlert))} />
</Col>
<Col xs={2} className={"centered-button-div"}>
<ToggleButton
grayscale={grayscale?.z}
disabled={disable?.z}
dim={!zParam.consistent}
toggleValue={zParam.value}
toggleAction={() =>
dispatch(settingToggle(z, sourceFwConfig, displayAlert))} />
</Col>
</Highlight>
</Row>;
}

View File

@ -4,8 +4,8 @@ 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 { Content } from "../../constants";
import { isBotOnline } from "../must_be_online";
import { Content, DeviceSetting } from "../../constants";
import { TimezoneSelector } from "../timezones/timezone_selector";
import { timezoneMismatch } from "../timezones/guess_timezone";
import { CameraSelection } from "./fbos_settings/camera_selection";
@ -15,6 +15,8 @@ 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";
import { Highlight } from "./maybe_highlight";
export enum ColWidth {
label = 3,
@ -22,15 +24,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() {
@ -87,83 +86,79 @@ export class FarmbotOsSettings
</WidgetHeader>
<WidgetBody>
<Row>
<Col xs={ColWidth.label}>
<label>
{t("NAME")}
</label>
</Col>
<Col xs={9}>
<input name="name"
onChange={this.changeBot}
onBlur={this.updateBot}
value={this.props.deviceAccount.body.name} />
</Col>
<Highlight settingName={DeviceSetting.name}>
<Col xs={ColWidth.label}>
<label>
{t(DeviceSetting.name)}
</label>
</Col>
<Col xs={9}>
<input name="name"
onChange={this.changeBot}
onBlur={this.updateBot}
value={this.props.deviceAccount.body.name} />
</Col>
</Highlight>
</Row>
<Row>
<Col xs={ColWidth.label}>
<label>
{t("TIME ZONE")}
</label>
</Col>
<Col xs={ColWidth.description}>
<div className="note">
{this.maybeWarnTz()}
</div>
<div>
<TimezoneSelector
currentTimezone={this.props.deviceAccount.body.timezone}
onUpdate={this.handleTimezone} />
</div>
</Col>
<Highlight settingName={DeviceSetting.timezone}>
<Col xs={ColWidth.label}>
<label>
{t("TIME ZONE")}
</label>
</Col>
<Col xs={ColWidth.description}>
<div className="note">
{this.maybeWarnTz()}
</div>
<div>
<TimezoneSelector
currentTimezone={this.props.deviceAccount.body.timezone}
onUpdate={this.handleTimezone} />
</div>
</Col>
</Highlight>
</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

@ -1,32 +1,35 @@
import * as React from "react";
import { Row, Col } from "../../../ui/index";
import { ToggleButton } from "../../../controls/toggle_button";
import { Content } from "../../../constants";
import { Content, DeviceSetting } from "../../../constants";
import { updateConfig } from "../../actions";
import { ColWidth } from "../farmbot_os_settings";
import { AutoSyncRowProps } from "./interfaces";
import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight";
export function AutoSyncRow(props: AutoSyncRowProps) {
const autoSync = props.sourceFbosConfig("auto_sync");
return <Row>
<Col xs={ColWidth.label}>
<label>
{t("AUTO SYNC")}
</label>
</Col>
<Col xs={ColWidth.description}>
<p>
{t(Content.AUTO_SYNC)}
</p>
</Col>
<Col xs={ColWidth.button}>
<ToggleButton
toggleValue={autoSync.value}
dim={!autoSync.consistent}
toggleAction={() => {
props.dispatch(updateConfig({ auto_sync: !autoSync.value }));
}} />
</Col>
<Highlight settingName={DeviceSetting.autoSync}>
<Col xs={ColWidth.label}>
<label>
{t("AUTO SYNC")}
</label>
</Col>
<Col xs={ColWidth.description}>
<p>
{t(Content.AUTO_SYNC)}
</p>
</Col>
<Col xs={ColWidth.button}>
<ToggleButton
toggleValue={autoSync.value}
dim={!autoSync.consistent}
toggleAction={() => {
props.dispatch(updateConfig({ auto_sync: !autoSync.value }));
}} />
</Col>
</Highlight>
</Row>;
}

View File

@ -3,39 +3,41 @@ import { Row, Col } from "../../../ui/index";
import { ColWidth } from "../farmbot_os_settings";
import { ToggleButton } from "../../../controls/toggle_button";
import { updateConfig } from "../../actions";
import { Content } from "../../../constants";
import { Content, DeviceSetting } from "../../../constants";
import { AutoUpdateRowProps } from "./interfaces";
import { t } from "../../../i18next_wrapper";
import { OtaTimeSelector, changeOtaHour } from "./ota_time_selector";
import { Feature } from "../../interfaces";
import { Highlight } from "../maybe_highlight";
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>
{t("FARMBOT OS AUTO UPDATE")}
</label>
</Col>
<Col xs={ColWidth.description}>
<p>
{t(Content.OS_AUTO_UPDATE)}
</p>
</Col>
<Col xs={ColWidth.button}>
<ToggleButton toggleValue={osAutoUpdate.value}
dim={!osAutoUpdate.consistent}
toggleAction={() => props.dispatch(updateConfig({
os_auto_update: !osAutoUpdate.value
}))} />
</Col>
<Highlight settingName={DeviceSetting.farmbotOSAutoUpdate}>
<Col xs={ColWidth.label}>
<label>
{t(DeviceSetting.farmbotOSAutoUpdate)}
</label>
</Col>
<Col xs={ColWidth.description}>
<p>
{t(Content.OS_AUTO_UPDATE)}
</p>
</Col>
<Col xs={ColWidth.button}>
<ToggleButton toggleValue={osAutoUpdate.value}
dim={!osAutoUpdate.consistent}
toggleAction={() => props.dispatch(updateConfig({
os_auto_update: !osAutoUpdate.value
}))} />
</Col>
</Highlight>
</Row>
</div>;
}

View File

@ -10,6 +10,8 @@ import { FirmwareHardwareStatus } from "./firmware_hardware_status";
import {
isFwHardwareValue, getFirmwareChoices, FIRMWARE_CHOICES_DDI
} from "../firmware_hardware_support";
import { Highlight } from "../maybe_highlight";
import { DeviceSetting } from "../../../constants";
interface BoardTypeState { sending: boolean }
@ -47,31 +49,32 @@ export class BoardType extends React.Component<BoardTypeProps, BoardTypeState> {
render() {
return <Row>
<Col xs={ColWidth.label}>
<label>
{t("FIRMWARE")}
</label>
</Col>
<Col xs={ColWidth.description}>
<div>
<FBSelect
key={this.apiValue}
extraClass={this.state.sending ? "dim" : ""}
list={getFirmwareChoices(this.props.shouldDisplay)}
selectedItem={this.selectedBoard}
onChange={this.sendOffConfig} />
</div>
</Col>
<Col xs={ColWidth.button}>
<FirmwareHardwareStatus
botOnline={this.props.botOnline}
apiFirmwareValue={this.apiValue}
alerts={this.props.alerts}
bot={this.props.bot}
dispatch={this.props.dispatch}
timeSettings={this.props.timeSettings}
shouldDisplay={this.props.shouldDisplay} />
</Col>
<Highlight settingName={DeviceSetting.firmware}>
<Col xs={ColWidth.label}>
<label>
{t("FIRMWARE")}
</label>
</Col>
<Col xs={ColWidth.description}>
<div>
<FBSelect
key={this.apiValue}
extraClass={this.state.sending ? "dim" : ""}
list={getFirmwareChoices()}
selectedItem={this.selectedBoard}
onChange={this.sendOffConfig} />
</div>
</Col>
<Col xs={ColWidth.button}>
<FirmwareHardwareStatus
botOnline={this.props.botOnline}
apiFirmwareValue={this.apiValue}
alerts={this.props.alerts}
bot={this.props.bot}
dispatch={this.props.dispatch}
timeSettings={this.props.timeSettings} />
</Col>
</Highlight>
</Row>;
}
}

View File

@ -9,6 +9,8 @@ import { selectAllSequences, findSequenceById } from "../../../resources/selecto
import { betterCompact } from "../../../util";
import { ColWidth } from "../farmbot_os_settings";
import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight";
import { DeviceSetting } from "../../../constants";
interface Props {
list: DropDownItem[];
@ -56,18 +58,20 @@ export class RawBootSequenceSelector extends React.Component<Props, {}> {
render() {
return <Row>
<Col xs={ColWidth.label}>
<label>
{t("BOOT SEQUENCE")}
</label>
</Col>
<Col xs={7}>
<FBSelect
allowEmpty={true}
list={this.props.list}
selectedItem={this.props.selectedItem}
onChange={this.onChange} />
</Col>
<Highlight settingName={DeviceSetting.bootSequence}>
<Col xs={ColWidth.label}>
<label>
{t("BOOT SEQUENCE")}
</label>
</Col>
<Col xs={7}>
<FBSelect
allowEmpty={true}
list={this.props.list}
selectedItem={this.props.selectedItem}
onChange={this.onChange} />
</Col>
</Highlight>
</Row>;
}
}

View File

@ -8,7 +8,8 @@ import { getDevice } from "../../../device";
import { ColWidth } from "../farmbot_os_settings";
import { Feature, UserEnv } from "../../interfaces";
import { t } from "../../../i18next_wrapper";
import { Content, ToolTips } from "../../../constants";
import { Content, ToolTips, DeviceSetting } from "../../../constants";
import { Highlight } from "../maybe_highlight";
/** Check if the camera has been disabled. */
export const cameraDisabled = (env: UserEnv): boolean =>
@ -84,21 +85,23 @@ export class CameraSelection
render() {
return <Row>
<Col xs={ColWidth.label}>
<label>
{t("CAMERA")}
</label>
</Col>
<Col xs={ColWidth.description}>
<div>
<FBSelect
allowEmpty={false}
list={CAMERA_CHOICES()}
selectedItem={this.selectedCamera()}
onChange={this.sendOffConfig}
extraClass={this.props.botOnline ? "" : "disabled"} />
</div>
</Col>
<Highlight settingName={DeviceSetting.camera}>
<Col xs={ColWidth.label}>
<label>
{t("CAMERA")}
</label>
</Col>
<Col xs={ColWidth.description}>
<div>
<FBSelect
allowEmpty={false}
list={CAMERA_CHOICES()}
selectedItem={this.selectedCamera()}
onChange={this.sendOffConfig}
extraClass={this.props.botOnline ? "" : "disabled"} />
</div>
</Col>
</Highlight>
</Row>;
}
}

View File

@ -1,12 +1,13 @@
import * as React from "react";
import { Row, Col } from "../../../ui/index";
import { Content } from "../../../constants";
import { Content, DeviceSetting } from "../../../constants";
import { factoryReset, updateConfig } from "../../actions";
import { ToggleButton } from "../../../controls/toggle_button";
import { BotConfigInputBox } from "../bot_config_input_box";
import { FactoryResetRowProps } from "./interfaces";
import { ColWidth } from "../farmbot_os_settings";
import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight";
export function FactoryResetRow(props: FactoryResetRowProps) {
const { dispatch, sourceFbosConfig, botOnline } = props;
@ -14,66 +15,72 @@ export function FactoryResetRow(props: FactoryResetRowProps) {
const maybeDisableTimer = disableFactoryReset.value ? { color: "grey" } : {};
return <div>
<Row>
<Col xs={ColWidth.label}>
<label>
{t("Factory Reset")}
</label>
</Col>
<Col xs={ColWidth.description}>
<p>
{t(Content.FACTORY_RESET_WARNING)}
</p>
</Col>
<Col xs={ColWidth.button}>
<button
className="fb-button red"
type="button"
onClick={factoryReset}
disabled={!botOnline}>
{t("FACTORY RESET")}
</button>
</Col>
<Highlight settingName={DeviceSetting.factoryReset}>
<Col xs={ColWidth.label}>
<label>
{t(DeviceSetting.factoryReset)}
</label>
</Col>
<Col xs={ColWidth.description}>
<p>
{t(Content.FACTORY_RESET_WARNING)}
</p>
</Col>
<Col xs={ColWidth.button}>
<button
className="fb-button red"
type="button"
onClick={factoryReset}
disabled={!botOnline}>
{t("FACTORY RESET")}
</button>
</Col>
</Highlight>
</Row>
<Row>
<Col xs={ColWidth.label}>
<label>
{t("Automatic Factory Reset")}
</label>
</Col>
<Col xs={ColWidth.description}>
<p>
{t(Content.AUTO_FACTORY_RESET)}
</p>
</Col>
<Col xs={ColWidth.button}>
<ToggleButton
toggleValue={!disableFactoryReset.value}
dim={!disableFactoryReset.consistent}
toggleAction={() => {
dispatch(updateConfig({
disable_factory_reset: !disableFactoryReset.value
}));
}} />
</Col>
<Highlight settingName={DeviceSetting.autoFactoryReset}>
<Col xs={ColWidth.label}>
<label>
{t(DeviceSetting.autoFactoryReset)}
</label>
</Col>
<Col xs={ColWidth.description}>
<p>
{t(Content.AUTO_FACTORY_RESET)}
</p>
</Col>
<Col xs={ColWidth.button}>
<ToggleButton
toggleValue={!disableFactoryReset.value}
dim={!disableFactoryReset.consistent}
toggleAction={() => {
dispatch(updateConfig({
disable_factory_reset: !disableFactoryReset.value
}));
}} />
</Col>
</Highlight>
</Row>
<Row>
<Col xs={ColWidth.label}>
<label style={maybeDisableTimer}>
{t("Connection Attempt Period")}
</label>
</Col>
<Col xs={ColWidth.description}>
<p style={maybeDisableTimer}>
{t(Content.AUTO_FACTORY_RESET_PERIOD)}
</p>
</Col>
<Col xs={ColWidth.button}>
<BotConfigInputBox
setting="network_not_found_timer"
dispatch={dispatch}
disabled={!!disableFactoryReset.value}
sourceFbosConfig={sourceFbosConfig} />
</Col>
<Highlight settingName={DeviceSetting.connectionAttemptPeriod}>
<Col xs={ColWidth.label}>
<label style={maybeDisableTimer}>
{t(DeviceSetting.connectionAttemptPeriod)}
</label>
</Col>
<Col xs={ColWidth.description}>
<p style={maybeDisableTimer}>
{t(Content.AUTO_FACTORY_RESET_PERIOD)}
</p>
</Col>
<Col xs={ColWidth.button}>
<BotConfigInputBox
setting="network_not_found_timer"
dispatch={dispatch}
disabled={!!disableFactoryReset.value}
sourceFbosConfig={sourceFbosConfig} />
</Col>
</Highlight>
</Row>
</div>;
}

View File

@ -7,6 +7,8 @@ import { FarmbotOsRowProps } from "./interfaces";
import { FbosDetails } from "./fbos_details";
import { t } from "../../../i18next_wrapper";
import { ErrorBoundary } from "../../../error_boundary";
import { Highlight } from "../maybe_highlight";
import { DeviceSetting } from "../../../constants";
const getVersionString =
(fbosVersion: string | undefined, onBeta: boolean | undefined): string => {
@ -21,48 +23,50 @@ export function FarmbotOsRow(props: FarmbotOsRowProps) {
} = bot.hardware.informational_settings;
const version = getVersionString(controller_version, currently_on_beta);
return <Row>
<Col xs={ColWidth.label}>
<label>
{t("FARMBOT OS")}
</label>
</Col>
<Col xs={3}>
<Popover position={Position.BOTTOM_LEFT}>
<p>
{t("Version {{ version }}", { version })}
</p>
<ErrorBoundary>
<FbosDetails
botInfoSettings={bot.hardware.informational_settings}
dispatch={dispatch}
shouldDisplay={props.shouldDisplay}
sourceFbosConfig={sourceFbosConfig}
botToMqttLastSeen={props.botToMqttLastSeen}
timeSettings={props.timeSettings}
deviceAccount={props.deviceAccount} />
</ErrorBoundary>
</Popover>
</Col>
<Col xs={3}>
<Popover position={Position.BOTTOM}>
<p className="release-notes-button">
{t("Release Notes")}&nbsp;
<Highlight settingName={DeviceSetting.farmbotOS}>
<Col xs={ColWidth.label}>
<label>
{t(DeviceSetting.farmbotOS)}
</label>
</Col>
<Col xs={3}>
<Popover position={Position.BOTTOM_LEFT}>
<p>
{t("Version {{ version }}", { version })}
</p>
<ErrorBoundary>
<FbosDetails
botInfoSettings={bot.hardware.informational_settings}
dispatch={dispatch}
shouldDisplay={props.shouldDisplay}
sourceFbosConfig={sourceFbosConfig}
botToMqttLastSeen={props.botToMqttLastSeen}
timeSettings={props.timeSettings}
deviceAccount={props.deviceAccount} />
</ErrorBoundary>
</Popover>
</Col>
<Col xs={3}>
<Popover position={Position.BOTTOM}>
<p className="release-notes-button">
{t("Release Notes")}&nbsp;
<i className="fa fa-caret-down" />
</p>
<div className="release-notes">
<h1>{props.osReleaseNotesHeading}</h1>
<Markdown>
{osReleaseNotes}
</Markdown>
</div>
</Popover>
</Col>
<Col xs={3}>
<OsUpdateButton
bot={bot}
sourceFbosConfig={sourceFbosConfig}
shouldDisplay={props.shouldDisplay}
botOnline={botOnline} />
</Col>
</p>
<div className="release-notes">
<h1>{props.osReleaseNotesHeading}</h1>
<Markdown>
{osReleaseNotes}
</Markdown>
</div>
</Popover>
</Col>
<Col xs={3}>
<OsUpdateButton
bot={bot}
sourceFbosConfig={sourceFbosConfig}
shouldDisplay={props.shouldDisplay}
botOnline={botOnline} />
</Col>
</Highlight>
</Row>;
}

View File

@ -2,10 +2,12 @@ import * as React from "react";
import { Row, Col } from "../../../ui";
import { ColWidth } from "../farmbot_os_settings";
import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight";
import { DeviceSetting } from "../../../constants";
export interface FbosButtonRowProps {
botOnline: boolean;
label: string;
label: DeviceSetting;
description: string;
buttonText: string;
color: string;
@ -14,24 +16,26 @@ export interface FbosButtonRowProps {
export const FbosButtonRow = (props: FbosButtonRowProps) => {
return <Row>
<Col xs={ColWidth.label}>
<label>
{t(props.label)}
</label>
</Col>
<Col xs={ColWidth.description}>
<p>
{t(props.description)}
</p>
</Col>
<Col xs={ColWidth.button}>
<button
className={`fb-button ${props.color}`}
type="button"
onClick={props.action}
disabled={!props.botOnline}>
{t(props.buttonText)}
</button>
</Col>
<Highlight settingName={props.label}>
<Col xs={ColWidth.label}>
<label>
{t(props.label)}
</label>
</Col>
<Col xs={ColWidth.description}>
<p>
{t(props.description)}
</p>
</Col>
<Col xs={ColWidth.button}>
<button
className={`fb-button ${props.color}`}
type="button"
onClick={props.action}
disabled={!props.botOnline}>
{t(props.buttonText)}
</button>
</Col>
</Highlight>
</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,59 +4,58 @@ 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 { Content, DeviceSetting } from "../../../constants";
import { reboot, powerOff, restartFirmware } from "../../actions";
import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight";
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>
return <Highlight className={"section"}
settingName={DeviceSetting.powerAndReset}>
<Header
expanded={power_and_reset}
title={DeviceSetting.powerAndReset}
panel={"power_and_reset"}
dispatch={dispatch} />
<Collapse isOpen={!!power_and_reset}>
<FbosButtonRow
botOnline={botOnline}
label={t("RESTART FARMBOT")}
label={DeviceSetting.restartFarmbot}
description={Content.RESTART_FARMBOT}
buttonText={t("RESTART")}
color={"yellow"}
action={reboot} />
<FbosButtonRow
botOnline={botOnline}
label={t("SHUTDOWN FARMBOT")}
label={DeviceSetting.shutdownFarmbot}
description={Content.SHUTDOWN_FARMBOT}
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={DeviceSetting.restartFirmware}
description={Content.RESTART_FIRMWARE}
buttonText={t("RESTART")}
color={"yellow"}
action={restartFirmware} />
<FactoryResetRow
dispatch={dispatch}
sourceFbosConfig={sourceFbosConfig}
botOnline={botOnline} />
{botOnline &&
<Popover position={Position.BOTTOM_LEFT}>
<p className={"release-notes-button"}>
{t("Change Ownership")}&nbsp;
<i className="fa fa-caret-down" />
</p>
<ChangeOwnershipForm />
</Popover>
}
<Highlight settingName={DeviceSetting.changeOwnership}>
<Popover position={Position.BOTTOM_LEFT}>
<p className={"release-notes-button"}>
{t(DeviceSetting.changeOwnership)}&nbsp;
<i className="fa fa-caret-down" />
</p>
<ChangeOwnershipForm />
</Popover>
</Highlight>}
</Collapse>
</section>;
</Highlight>;
}

View File

@ -1,5 +1,4 @@
import { FirmwareHardware } from "farmbot";
import { ShouldDisplay, Feature } from "../interfaces";
import { FirmwareHardware, TaggedFbosConfig } from "farmbot";
export const isFwHardwareValue = (x?: unknown): x is FirmwareHardware => {
const values: FirmwareHardware[] = [
@ -11,6 +10,12 @@ export const isFwHardwareValue = (x?: unknown): x is FirmwareHardware => {
return !!values.includes(x as FirmwareHardware);
};
export const getFwHardwareValue =
(fbosConfig: TaggedFbosConfig | undefined) => {
const value = fbosConfig?.body.firmware_hardware;
return isFwHardwareValue(value) ? value : undefined;
};
const TMC_BOARDS = ["express_k10", "farmduino_k15"];
const EXPRESS_BOARDS = ["express_k10"];
@ -77,12 +82,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,10 +16,16 @@ 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";
import { maybeOpenPanel } from "./maybe_highlight";
export class HardwareSettings extends
React.Component<HardwareSettingsProps, {}> {
componentDidMount = () =>
this.props.dispatch(maybeOpenPanel(this.props.controlPanelState));
render() {
const {
bot, dispatch, sourceFwConfig, controlPanelState, firmwareConfig,
@ -27,15 +34,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 +48,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,41 @@
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";
import { DeviceSetting } from "../../../../constants";
describe("<CalibrationRow />", () => {
const fakeProps = (): CalibrationRowProps => ({
type: "calibrate",
hardware: bot.hardware.mcu_params,
botDisconnected: false,
action: jest.fn(),
toolTip: "calibrate",
title: DeviceSetting.calibration,
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,16 +1,17 @@
import * as React from "react";
import { Header } from "../header";
import { mount } from "enzyme";
import { DeviceSetting } from "../../../../constants";
describe("<Header/>", () => {
it("renders", () => {
const fn = jest.fn();
const el = mount(<Header
title="FOO"
title={DeviceSetting.motors}
expanded={true}
name={"motors"}
panel={"motors"}
dispatch={fn} />);
expect(el.text()).toContain("FOO");
expect(el.text().toLowerCase()).toContain("motors");
expect(el.find(".fa-minus").length).toBe(1);
});
});

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,40 +1,37 @@
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"));
import { Highlight } from "../maybe_highlight";
export function CalibrationRow(props: CalibrationRowProps) {
const { hardware, botDisconnected } = props;
return <Row>
<Col xs={6} className={"widget-body-tooltips"}>
<label>
{t("CALIBRATION")}
</label>
<Help text={ToolTips.CALIBRATION} 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={() => calibrate(axis)}>
{t("CALIBRATE {{axis}}", { axis })}
</LockableButton>
</Col>;
})}
<Highlight settingName={props.title}>
<Col xs={6} className={"widget-body-tooltips"}>
<label>
{t(props.title)}
</label>
<Help text={t(props.toolTip)}
requireClick={true} position={Position.RIGHT} />
</Col>
{axisTrackingStatus(hardware)
.map(row => {
const { axis } = row;
const hardwareDisabled = props.type == "zero" ? false : row.disabled;
return <Col xs={2} key={axis} className={"centered-button-div"}>
<LockableButton
disabled={hardwareDisabled || botDisconnected}
onClick={() => props.action(axis)}>
{`${t(props.axisTitle)} ${axis}`}
</LockableButton>
</Col>;
})}
</Highlight>
</Row>;
}

View File

@ -3,41 +3,45 @@ import { DangerZoneProps } from "../interfaces";
import { Row, Col } from "../../../ui/index";
import { Header } from "./header";
import { Collapse } from "@blueprintjs/core";
import { Content } from "../../../constants";
import { Content, DeviceSetting } from "../../../constants";
import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight";
export function DangerZone(props: DangerZoneProps) {
const { dispatch, onReset, botDisconnected } = props;
const { danger_zone } = props.controlPanelState;
return <section>
return <Highlight className={"section"}
settingName={DeviceSetting.dangerZone}>
<Header
expanded={danger_zone}
title={t("Danger Zone")}
name={"danger_zone"}
title={DeviceSetting.dangerZone}
panel={"danger_zone"}
dispatch={dispatch} />
<Collapse isOpen={!!danger_zone}>
<Row>
<Col xs={4}>
<label>
{t("Reset hardware parameter defaults")}
</label>
</Col>
<Col xs={6}>
<p>
{t(Content.RESTORE_DEFAULT_HARDWARE_SETTINGS)}
</p>
</Col>
<Col xs={2} className={"centered-button-div"}>
<button
className="fb-button red"
disabled={botDisconnected}
onClick={onReset}>
{t("RESET")}
</button>
</Col>
<Highlight settingName={DeviceSetting.resetHardwareParams}>
<Col xs={4}>
<label>
{t(DeviceSetting.resetHardwareParams)}
</label>
</Col>
<Col xs={6}>
<p>
{t(Content.RESTORE_DEFAULT_HARDWARE_SETTINGS)}
</p>
</Col>
<Col xs={2} className={"centered-button-div"}>
<button
className="fb-button red"
disabled={botDisconnected}
onClick={onReset}>
{t("RESET")}
</button>
</Col>
</Highlight>
</Row>
</Collapse>
</section>;
</Highlight>;
}

View File

@ -0,0 +1,111 @@
import * as React from "react";
import { BooleanMCUInputGroup } from "../boolean_mcu_input_group";
import { ToolTips, DeviceSetting } from "../../../constants";
import { NumericMCUInputGroup } from "../numeric_mcu_input_group";
import { EncodersProps } from "../interfaces";
import { Header } from "./header";
import { Collapse } from "@blueprintjs/core";
import { isExpressBoard } from "../firmware_hardware_support";
import { Highlight } from "../maybe_highlight";
export function Encoders(props: EncodersProps) {
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 <Highlight className={"section"}
settingName={DeviceSetting.encoders}>
<Header
expanded={encoders}
title={isExpress
? DeviceSetting.stallDetection
: DeviceSetting.encoders}
panel={"encoders"}
dispatch={dispatch} />
<Collapse isOpen={!!encoders}>
<BooleanMCUInputGroup
label={isExpress
? DeviceSetting.enableStallDetection
: DeviceSetting.enableEncoders}
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} />
{isExpress &&
<NumericMCUInputGroup
label={DeviceSetting.stallSensitivity}
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
label={DeviceSetting.useEncodersForPositioning}
tooltip={ToolTips.ENCODER_POSITIONING}
x={"encoder_use_for_pos_x"}
y={"encoder_use_for_pos_y"}
z={"encoder_use_for_pos_z"}
grayscale={encodersDisabled}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />}
{!isExpress &&
<BooleanMCUInputGroup
label={DeviceSetting.invertEncoders}
tooltip={ToolTips.INVERT_ENCODERS}
x={"encoder_invert_x"}
y={"encoder_invert_y"}
z={"encoder_invert_z"}
grayscale={encodersDisabled}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />}
<NumericMCUInputGroup
label={DeviceSetting.maxMissedSteps}
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"}
gray={encodersDisabled}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />
<NumericMCUInputGroup
label={DeviceSetting.missedStepDecay}
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} />
{!isExpress &&
<NumericMCUInputGroup
label={DeviceSetting.encoderScaling}
tooltip={ToolTips.ENCODER_SCALING}
x={"encoder_scaling_x"}
y={"encoder_scaling_y"}
z={"encoder_scaling_z"}
xScale={sourceFwConfig("movement_microsteps_x").value}
yScale={sourceFwConfig("movement_microsteps_y").value}
zScale={sourceFwConfig("movement_microsteps_z").value}
intSize={"long"}
gray={encodersDisabled}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />}
</Collapse>
</Highlight>;
}

View File

@ -1,130 +0,0 @@
import * as React from "react";
import { BooleanMCUInputGroup } from "../boolean_mcu_input_group";
import { ToolTips } from "../../../constants";
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) {
const { encoders_and_endstops } = props.controlPanelState;
const { dispatch, sourceFwConfig, shouldDisplay, firmwareHardware } = props;
const encodersDisabled = {
x: !sourceFwConfig("encoder_enabled_x").value,
y: !sourceFwConfig("encoder_enabled_y").value,
z: !sourceFwConfig("encoder_enabled_z").value
};
return <section>
<Header
expanded={encoders_and_endstops}
title={isExpressBoard(firmwareHardware)
? t("Stall Detection and Endstops")
: t("Encoders and Endstops")}
name={"encoders_and_endstops"}
dispatch={dispatch} />
<Collapse isOpen={!!encoders_and_endstops}>
<BooleanMCUInputGroup
name={isExpressBoard(firmwareHardware)
? t("Enable Stall Detection")
: t("Enable Encoders")}
tooltip={ToolTips.ENABLE_ENCODERS}
x={"encoder_enabled_x"}
y={"encoder_enabled_y"}
z={"encoder_enabled_z"}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />
{!isExpressBoard(firmwareHardware) &&
<BooleanMCUInputGroup
name={t("Use Encoders for Positioning")}
tooltip={ToolTips.ENCODER_POSITIONING}
x={"encoder_use_for_pos_x"}
y={"encoder_use_for_pos_y"}
z={"encoder_use_for_pos_z"}
grayscale={encodersDisabled}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />}
{!isExpressBoard(firmwareHardware) &&
<BooleanMCUInputGroup
name={t("Invert Encoders")}
tooltip={ToolTips.INVERT_ENCODERS}
x={"encoder_invert_x"}
y={"encoder_invert_y"}
z={"encoder_invert_z"}
grayscale={encodersDisabled}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />}
<NumericMCUInputGroup
name={t("Max Missed Steps")}
tooltip={ToolTips.MAX_MISSED_STEPS}
x={"encoder_missed_steps_max_x"}
y={"encoder_missed_steps_max_y"}
z={"encoder_missed_steps_max_z"}
gray={encodersDisabled}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />
<NumericMCUInputGroup
name={t("Missed Step Decay")}
tooltip={ToolTips.ENCODER_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) &&
<NumericMCUInputGroup
name={t("Encoder Scaling")}
tooltip={ToolTips.ENCODER_SCALING}
x={"encoder_scaling_x"}
y={"encoder_scaling_y"}
z={"encoder_scaling_z"}
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"}
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,58 @@
import * as React from "react";
import { BooleanMCUInputGroup } from "../boolean_mcu_input_group";
import { ToolTips, DeviceSetting } from "../../../constants";
import { EndStopsProps } from "../interfaces";
import { Header } from "./header";
import { Collapse } from "@blueprintjs/core";
import { Highlight } from "../maybe_highlight";
export function EndStops(props: EndStopsProps) {
const { endstops } = props.controlPanelState;
const { dispatch, sourceFwConfig } = props;
return <Highlight className={"section"}
settingName={DeviceSetting.endstops}>
<Header
expanded={endstops}
title={DeviceSetting.endstops}
panel={"endstops"}
dispatch={dispatch} />
<Collapse isOpen={!!endstops}>
<BooleanMCUInputGroup
label={DeviceSetting.enableEndstops}
tooltip={ToolTips.ENABLE_ENDSTOPS}
x={"movement_enable_endpoints_x"}
y={"movement_enable_endpoints_y"}
z={"movement_enable_endpoints_z"}
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />
<BooleanMCUInputGroup
label={DeviceSetting.swapEndstops}
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
label={DeviceSetting.invertEndstops}
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>
</Highlight>;
}

View File

@ -0,0 +1,54 @@
import * as React from "react";
import { NumericMCUInputGroup } from "../numeric_mcu_input_group";
import { ToolTips, DeviceSetting } from "../../../constants";
import { ErrorHandlingProps } from "../interfaces";
import { Header } from "./header";
import { Collapse } from "@blueprintjs/core";
import { McuInputBox } from "../mcu_input_box";
import { settingToggle } from "../../actions";
import { SingleSettingRow } from "./single_setting_row";
import { ToggleButton } from "../../../controls/toggle_button";
import { Highlight } from "../maybe_highlight";
export function ErrorHandling(props: ErrorHandlingProps) {
const { error_handling } = props.controlPanelState;
const { dispatch, sourceFwConfig } = props;
const eStopOnMoveError = sourceFwConfig("param_e_stop_on_mov_err");
return <Highlight className={"section"}
settingName={DeviceSetting.errorHandling}>
<Header
expanded={error_handling}
title={DeviceSetting.errorHandling}
panel={"error_handling"}
dispatch={dispatch} />
<Collapse isOpen={!!error_handling}>
<NumericMCUInputGroup
label={DeviceSetting.timeoutAfter}
tooltip={ToolTips.TIMEOUT_AFTER}
x={"movement_timeout_x"}
y={"movement_timeout_y"}
z={"movement_timeout_z"}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />
<SingleSettingRow settingType="input"
label={DeviceSetting.maxRetries}
tooltip={ToolTips.MAX_MOVEMENT_RETRIES}>
<McuInputBox
setting="param_mov_nr_retry"
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />
</SingleSettingRow>
<SingleSettingRow settingType="button"
label={DeviceSetting.estopOnMovementError}
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>
</Highlight>;
}

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,18 +2,20 @@ import * as React from "react";
import { ControlPanelState } from "../../interfaces";
import { toggleControlPanel } from "../../actions";
import { ExpandableHeader } from "../../../ui/expandable_header";
import { t } from "../../../i18next_wrapper";
import { DeviceSetting } from "../../../constants";
interface Props {
dispatch: Function;
name: keyof ControlPanelState;
title: string;
panel: keyof ControlPanelState;
title: DeviceSetting;
expanded: boolean;
}
export const Header = (props: Props) => {
const { dispatch, name, title, expanded } = props;
const { dispatch, panel, title, expanded } = props;
return <ExpandableHeader
expanded={expanded}
title={title}
onClick={() => dispatch(toggleControlPanel(name))} />;
title={t(title)}
onClick={() => dispatch(toggleControlPanel(panel))} />;
};

View File

@ -1,20 +1,25 @@
import * as React from "react";
import { BooleanMCUInputGroup } from "../boolean_mcu_input_group";
import { ToolTips } from "../../../constants";
import { ToolTips, DeviceSetting } 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";
import { Highlight } from "../maybe_highlight";
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;
@ -27,19 +32,51 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
const scale = calculateScale(sourceFwConfig);
return <section>
return <Highlight className={"section"}
settingName={DeviceSetting.homingAndCalibration}>
<Header
title={t("Homing and Calibration")}
name={"homing_and_calibration"}
title={DeviceSetting.homingAndCalibration}
panel={"homing_and_calibration"}
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={DeviceSetting.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={DeviceSetting.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={DeviceSetting.setZeroPosition}
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}
label={DeviceSetting.findHomeOnBoot}
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"}
@ -48,7 +85,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
sourceFwConfig={sourceFwConfig}
caution={true} />
<BooleanMCUInputGroup
name={t("Stop at Home")}
label={DeviceSetting.stopAtHome}
tooltip={ToolTips.STOP_AT_HOME}
x={"movement_stop_at_home_x"}
y={"movement_stop_at_home_y"}
@ -56,7 +93,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />
<BooleanMCUInputGroup
name={t("Stop at Max")}
label={DeviceSetting.stopAtMax}
tooltip={ToolTips.STOP_AT_MAX}
x={"movement_stop_at_max_x"}
y={"movement_stop_at_max_y"}
@ -64,7 +101,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />
<BooleanMCUInputGroup
name={t("Negative Coordinates Only")}
label={DeviceSetting.negativeCoordinatesOnly}
tooltip={ToolTips.NEGATIVE_COORDINATES_ONLY}
x={"movement_home_up_x"}
y={"movement_home_up_y"}
@ -72,7 +109,7 @@ export function HomingAndCalibration(props: HomingAndCalibrationProps) {
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />
<NumericMCUInputGroup
name={t("Axis Length (mm)")}
label={DeviceSetting.axisLength}
tooltip={ToolTips.LENGTH}
x={"movement_axis_nr_steps_x"}
y={"movement_axis_nr_steps_y"}
@ -88,14 +125,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>;
</Highlight>;
}

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

@ -1,36 +1,18 @@
import * as React from "react";
import { BooleanMCUInputGroup } from "../boolean_mcu_input_group";
import { ToolTips } from "../../../constants";
import { ToolTips, DeviceSetting } from "../../../constants";
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 { t } from "../../../i18next_wrapper";
import { Collapse } from "@blueprintjs/core";
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";
import { Highlight } from "../maybe_highlight";
export const calculateScale =
(sourceFwConfig: SourceFwConfig): Record<Xyz, number | undefined> => {
@ -51,39 +33,18 @@ 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>
return <Highlight className={"section"}
settingName={DeviceSetting.motors}>
<Header
expanded={controlPanelState.motors}
title={t("Motors")}
name={"motors"}
title={DeviceSetting.motors}
panel={"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)")}
label={DeviceSetting.maxSpeed}
tooltip={ToolTips.MAX_SPEED}
x={"movement_max_spd_x"}
y={"movement_max_spd_y"}
@ -94,7 +55,7 @@ export function Motors(props: MotorsProps) {
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />
<NumericMCUInputGroup
name={t("Homing Speed (mm/s)")}
label={DeviceSetting.homingSpeed}
tooltip={ToolTips.HOME_SPEED}
x={"movement_home_spd_x"}
y={"movement_home_spd_y"}
@ -105,7 +66,7 @@ export function Motors(props: MotorsProps) {
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />
<NumericMCUInputGroup
name={t("Minimum Speed (mm/s)")}
label={DeviceSetting.minimumSpeed}
tooltip={ToolTips.MIN_SPEED}
x={"movement_min_spd_x"}
y={"movement_min_spd_y"}
@ -116,7 +77,7 @@ export function Motors(props: MotorsProps) {
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />
<NumericMCUInputGroup
name={t("Accelerate for (mm)")}
label={DeviceSetting.accelerateFor}
tooltip={ToolTips.ACCELERATE_FOR}
x={"movement_steps_acc_dec_x"}
y={"movement_steps_acc_dec_y"}
@ -127,7 +88,7 @@ export function Motors(props: MotorsProps) {
sourceFwConfig={sourceFwConfig}
dispatch={dispatch} />
<NumericMCUInputGroup
name={t("Steps per MM")}
label={DeviceSetting.stepsPerMm}
tooltip={ToolTips.STEPS_PER_MM}
x={"movement_step_per_mm_x"}
y={"movement_step_per_mm_y"}
@ -139,7 +100,7 @@ export function Motors(props: MotorsProps) {
sourceFwConfig={props.sourceFwConfig}
dispatch={props.dispatch} />
<NumericMCUInputGroup
name={t("Microsteps per step")}
label={DeviceSetting.microstepsPerStep}
tooltip={ToolTips.MICROSTEPS_PER_STEP}
x={"movement_microsteps_x"}
y={"movement_microsteps_y"}
@ -147,7 +108,7 @@ export function Motors(props: MotorsProps) {
sourceFwConfig={props.sourceFwConfig}
dispatch={props.dispatch} />
<BooleanMCUInputGroup
name={t("Always Power Motors")}
label={DeviceSetting.alwaysPowerMotors}
tooltip={ToolTips.ALWAYS_POWER_MOTORS}
x={"movement_keep_active_x"}
y={"movement_keep_active_y"}
@ -155,7 +116,7 @@ export function Motors(props: MotorsProps) {
dispatch={dispatch}
sourceFwConfig={sourceFwConfig} />
<BooleanMCUInputGroup
name={t("Invert Motors")}
label={DeviceSetting.invertMotors}
tooltip={ToolTips.INVERT_MOTORS}
x={"movement_invert_motor_x"}
y={"movement_invert_motor_y"}
@ -164,25 +125,15 @@ export function Motors(props: MotorsProps) {
sourceFwConfig={sourceFwConfig} />
{isTMCBoard(firmwareHardware) &&
<NumericMCUInputGroup
name={t("Motor Current")}
label={DeviceSetting.motorCurrent}
tooltip={ToolTips.MOTOR_CURRENT}
x={"movement_motor_current_x"}
y={"movement_motor_current_y"}
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")}
label={DeviceSetting.enable2ndXMotor}
tooltip={ToolTips.ENABLE_X2_MOTOR}>
<ToggleButton
toggleValue={enable2ndXMotor.value}
@ -191,7 +142,7 @@ export function Motors(props: MotorsProps) {
settingToggle("movement_secondary_motor_x", sourceFwConfig))} />
</SingleSettingRow>
<SingleSettingRow settingType="button"
label={t("Invert 2nd X Motor")}
label={DeviceSetting.invert2ndXMotor}
tooltip={ToolTips.INVERT_MOTORS}>
<ToggleButton
grayscale={!enable2ndXMotor.value}
@ -201,5 +152,5 @@ export function Motors(props: MotorsProps) {
settingToggle("movement_secondary_motor_invert_x", sourceFwConfig))} />
</SingleSettingRow>
</Collapse>
</section>;
</Highlight>;
}

View File

@ -0,0 +1,25 @@
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";
import { DeviceSetting } from "../../../constants";
import { Highlight } from "../maybe_highlight";
export function PinBindings(props: PinBindingsProps) {
const { pin_bindings } = props.controlPanelState;
const { dispatch, resources } = props;
return <Highlight className={"section"}
settingName={DeviceSetting.pinBindings}>
<Header
expanded={pin_bindings}
title={DeviceSetting.pinBindings}
panel={"pin_bindings"}
dispatch={dispatch} />
<Collapse isOpen={!!pin_bindings}>
<PinBindingsContent dispatch={dispatch} resources={resources} />
</Collapse>
</Highlight>;
}

View File

@ -4,19 +4,21 @@ import { PinGuardProps } from "../interfaces";
import { Header } from "./header";
import { Collapse, Position } from "@blueprintjs/core";
import { Row, Col, Help } from "../../../ui/index";
import { ToolTips } from "../../../constants";
import { ToolTips, DeviceSetting } from "../../../constants";
import { t } from "../../../i18next_wrapper";
import { Highlight } from "../maybe_highlight";
export function PinGuard(props: PinGuardProps) {
const { pin_guard } = props.controlPanelState;
const { dispatch, sourceFwConfig, resources } = props;
return <section>
return <Highlight className={"section"}
settingName={DeviceSetting.pinGuard}>
<Header
expanded={pin_guard}
title={t("Pin Guard")}
name={"pin_guard"}
title={DeviceSetting.pinGuard}
panel={"pin_guard"}
dispatch={dispatch} />
<Collapse isOpen={!!pin_guard}>
<Row>
@ -79,5 +81,5 @@ export function PinGuard(props: PinGuardProps) {
resources={resources}
sourceFwConfig={sourceFwConfig} />
</Collapse>
</section>;
</Highlight>;
}

View File

@ -0,0 +1,27 @@
import * as React from "react";
import { Row, Col, Help } from "../../../ui/index";
import { Position } from "@blueprintjs/core";
import { DeviceSetting } from "../../../constants";
import { Highlight } from "../maybe_highlight";
import { t } from "../../../i18next_wrapper";
export interface SingleSettingRowProps {
label: DeviceSetting;
tooltip: string;
children: React.ReactChild;
settingType: "button" | "input";
}
export const SingleSettingRow =
({ label, tooltip, settingType, children }: SingleSettingRowProps) =>
<Row>
<Highlight settingName={label}>
<Col xs={6} className={"widget-body-tooltips"}>
<label>{t(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>}
</Highlight>
</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,16 +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;
}
import { DeviceSetting } from "../../constants";
export interface ZeroRowProps {
botDisconnected: boolean;
@ -19,16 +15,18 @@ 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 {
sourceFwConfig: SourceFwConfig;
dispatch: Function;
tooltip: string;
name: string;
label: DeviceSetting;
x: McuParamName;
y: McuParamName;
z: McuParamName;
@ -39,15 +37,20 @@ export interface BooleanMCUInputGroupProps {
}
export interface CalibrationRowProps {
type: "find_home" | "calibrate" | "zero";
hardware: McuParams;
botDisconnected: boolean;
action(axis: Axis): void;
toolTip: string;
title: DeviceSetting;
axisTitle: string;
}
export interface NumericMCUInputGroupProps {
sourceFwConfig: SourceFwConfig;
dispatch: Function;
tooltip: string;
name: string;
label: DeviceSetting;
x: McuParamName;
xScale?: number;
y: McuParamName;
@ -85,12 +88,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

@ -0,0 +1,162 @@
import * as React from "react";
import { ControlPanelState } from "../interfaces";
import { toggleControlPanel } from "../actions";
import { urlFriendly } from "../../util";
import { DeviceSetting } from "../../constants";
const HOMING_PANEL = [
DeviceSetting.homingAndCalibration,
DeviceSetting.homing,
DeviceSetting.calibration,
DeviceSetting.setZeroPosition,
DeviceSetting.findHomeOnBoot,
DeviceSetting.stopAtHome,
DeviceSetting.stopAtMax,
DeviceSetting.negativeCoordinatesOnly,
DeviceSetting.axisLength,
];
const MOTORS_PANEL = [
DeviceSetting.motors,
DeviceSetting.maxSpeed,
DeviceSetting.homingSpeed,
DeviceSetting.minimumSpeed,
DeviceSetting.accelerateFor,
DeviceSetting.stepsPerMm,
DeviceSetting.microstepsPerStep,
DeviceSetting.alwaysPowerMotors,
DeviceSetting.invertMotors,
DeviceSetting.motorCurrent,
DeviceSetting.enable2ndXMotor,
DeviceSetting.invert2ndXMotor,
];
const ENCODERS_PANEL = [
DeviceSetting.encoders,
DeviceSetting.stallDetection,
DeviceSetting.enableEncoders,
DeviceSetting.enableStallDetection,
DeviceSetting.stallSensitivity,
DeviceSetting.useEncodersForPositioning,
DeviceSetting.invertEncoders,
DeviceSetting.maxMissedSteps,
DeviceSetting.missedStepDecay,
DeviceSetting.encoderScaling,
];
const ENDSTOPS_PANEL = [
DeviceSetting.endstops,
DeviceSetting.enableEndstops,
DeviceSetting.swapEndstops,
DeviceSetting.invertEndstops,
];
const ERROR_HANDLING_PANEL = [
DeviceSetting.errorHandling,
DeviceSetting.timeoutAfter,
DeviceSetting.maxRetries,
DeviceSetting.estopOnMovementError,
];
const PIN_GUARD_PANEL = [
DeviceSetting.pinGuard,
];
const DANGER_ZONE_PANEL = [
DeviceSetting.dangerZone,
DeviceSetting.resetHardwareParams,
];
const PIN_BINDINGS_PANEL = [
DeviceSetting.pinBindings,
];
const POWER_AND_RESET_PANEL = [
DeviceSetting.powerAndReset,
DeviceSetting.restartFarmbot,
DeviceSetting.shutdownFarmbot,
DeviceSetting.restartFirmware,
DeviceSetting.factoryReset,
DeviceSetting.autoFactoryReset,
DeviceSetting.connectionAttemptPeriod,
DeviceSetting.changeOwnership,
];
/** Look up parent panels for settings. */
const SETTING_PANEL_LOOKUP = {} as Record<DeviceSetting, keyof ControlPanelState>;
HOMING_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "homing_and_calibration");
MOTORS_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "motors");
ENCODERS_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "encoders");
ENDSTOPS_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "endstops");
ERROR_HANDLING_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "error_handling");
PIN_GUARD_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "pin_guard");
DANGER_ZONE_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "danger_zone");
PIN_BINDINGS_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "pin_bindings");
POWER_AND_RESET_PANEL.map(s => SETTING_PANEL_LOOKUP[s] = "power_and_reset");
/** Look up parent panels for settings using URL-friendly names. */
const URL_FRIENDLY_LOOKUP: Record<string, keyof ControlPanelState> = {};
Object.entries(SETTING_PANEL_LOOKUP).map(([setting, panel]) =>
URL_FRIENDLY_LOOKUP[urlFriendly(setting)] = panel);
/** Look up all relevant names for the same setting. */
const ALTERNATE_NAMES =
Object.values(DeviceSetting).reduce((acc, s) => { acc[s] = [s]; return acc; },
{} as Record<DeviceSetting, DeviceSetting[]>);
ALTERNATE_NAMES[DeviceSetting.encoders].push(DeviceSetting.stallDetection);
ALTERNATE_NAMES[DeviceSetting.stallDetection].push(DeviceSetting.encoders);
/** Generate array of names for the same setting. Most only have one. */
const compareValues = (settingName: DeviceSetting) =>
(ALTERNATE_NAMES[settingName]).map(s => urlFriendly(s));
/** Retrieve a highlight search term. */
const getHighlightName = () => location.search.split("?highlight=").pop();
/** Only open panel and highlight once per app load. Exported for tests. */
export const highlight = { opened: false, highlighted: false };
/** Open a panel if a setting in that panel is highlighted. */
export const maybeOpenPanel = (panelState: ControlPanelState) =>
(dispatch: Function) => {
if (highlight.opened) { return; }
const urlFriendlySettingName = urlFriendly(getHighlightName() || "");
if (!urlFriendlySettingName) { return; }
const panel = URL_FRIENDLY_LOOKUP[urlFriendlySettingName];
const panelIsOpen = panelState[panel];
if (panelIsOpen) { return; }
dispatch(toggleControlPanel(panel));
highlight.opened = true;
};
/** Highlight a setting if provided as a search term. */
export const maybeHighlight = (settingName: DeviceSetting) => {
const item = getHighlightName();
if (highlight.highlighted || !item) { return ""; }
const isCurrentSetting = compareValues(settingName).includes(item);
if (!isCurrentSetting) { return ""; }
highlight.highlighted = true;
return "highlight";
};
export interface HighlightProps {
settingName: DeviceSetting;
children: React.ReactChild
| React.ReactChild[]
| (React.ReactChild | React.ReactChild[])[];
className?: string;
}
interface HighlightState {
className: string;
}
/** Wrap highlight-able settings. */
export class Highlight extends React.Component<HighlightProps, HighlightState> {
state: HighlightState = { className: maybeHighlight(this.props.settingName) };
componentDidMount = () => {
if (this.state.className == "highlight") {
/** Slowly fades highlight. */
this.setState({ className: "unhighlight" });
}
}
render() {
return <div className={`${this.props.className} ${this.state.className}`}>
{this.props.children}
</div>;
}
}

View File

@ -3,48 +3,52 @@ import { McuInputBox } from "./mcu_input_box";
import { NumericMCUInputGroupProps } from "./interfaces";
import { Row, Col, Help } from "../../ui/index";
import { Position } from "@blueprintjs/core";
import { Highlight } from "./maybe_highlight";
import { t } from "../../i18next_wrapper";
export function NumericMCUInputGroup(props: NumericMCUInputGroupProps) {
const {
sourceFwConfig, dispatch, tooltip, name, x, y, z, intSize, gray, float,
sourceFwConfig, dispatch, tooltip, label, x, y, z, intSize, gray, float,
} = props;
return <Row>
<Col xs={6} className={"widget-body-tooltips"}>
<label>
{name}
</label>
<Help text={tooltip} requireClick={true} position={Position.RIGHT} />
</Col>
<Col xs={2}>
<McuInputBox
setting={x}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch}
intSize={intSize}
float={float}
scale={props.xScale}
gray={gray?.x} />
</Col>
<Col xs={2}>
<McuInputBox
setting={y}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch}
intSize={intSize}
float={float}
scale={props.yScale}
gray={gray?.y} />
</Col>
<Col xs={2}>
<McuInputBox
setting={z}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch}
intSize={intSize}
float={float}
scale={props.zScale}
gray={gray?.z} />
</Col>
<Highlight settingName={label}>
<Col xs={6} className={"widget-body-tooltips"}>
<label>
{t(label)}
</label>
<Help text={tooltip} requireClick={true} position={Position.RIGHT} />
</Col>
<Col xs={2}>
<McuInputBox
setting={x}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch}
intSize={intSize}
float={float}
scale={props.xScale}
gray={gray?.x} />
</Col>
<Col xs={2}>
<McuInputBox
setting={y}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch}
intSize={intSize}
float={float}
scale={props.yScale}
gray={gray?.y} />
</Col>
<Col xs={2}>
<McuInputBox
setting={z}
sourceFwConfig={sourceFwConfig}
dispatch={dispatch}
intSize={intSize}
float={float}
scale={props.zScale}
gray={gray?.z} />
</Col>
</Highlight>
</Row>;
}

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 {
@ -201,6 +201,7 @@ export interface PeripheralsProps {
peripherals: TaggedPeripheral[];
dispatch: Function;
disabled: boolean | undefined;
firmwareHardware: FirmwareHardware | undefined;
}
export interface SensorsProps {
@ -208,6 +209,7 @@ export interface SensorsProps {
sensors: TaggedSensor[];
dispatch: Function;
disabled: boolean | undefined;
firmwareHardware: FirmwareHardware | undefined;
}
export interface FarmwareProps {
@ -245,8 +247,11 @@ export interface HardwareSettingsProps {
export interface ControlPanelState {
homing_and_calibration: boolean;
motors: boolean;
encoders_and_endstops: boolean;
danger_zone: boolean;
power_and_reset: boolean;
encoders: boolean;
endstops: boolean;
error_handling: boolean;
pin_guard: boolean;
danger_zone: boolean;
pin_bindings: boolean;
power_and_reset: 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,
@ -84,17 +89,17 @@ export const piSpi1Pins = [16, 17, 18, 19, 20, 21];
/** Pin numbers used for special purposes by the RPi. (internal pullup, etc.) */
export const reservedPiGPIO = piI2c0Pins;
const LabeledGpioPins: { [x: number]: string } = {
[ButtonPin.estop]: "Button 1: E-STOP",
[ButtonPin.unlock]: "Button 2: UNLOCK",
[ButtonPin.btn3]: "Button 3",
[ButtonPin.btn4]: "Button 4",
[ButtonPin.btn5]: "Button 5",
};
const GPIO_PIN_LABELS = (): { [x: number]: string } => ({
[ButtonPin.estop]: t("Button {{ num }}: E-STOP", { num: 1 }),
[ButtonPin.unlock]: t("Button {{ num }}: UNLOCK", { num: 2 }),
[ButtonPin.btn3]: t("Button {{ num }})", { num: 3 }),
[ButtonPin.btn4]: t("Button {{ num }}", { num: 4 }),
[ButtonPin.btn5]: t("Button {{ num }}", { num: 5 }),
});
export const generatePinLabel = (pin: number) =>
LabeledGpioPins[pin]
? `${LabeledGpioPins[pin]} (Pi ${pin})`
GPIO_PIN_LABELS()[pin]
? `${t(GPIO_PIN_LABELS()[pin])} (Pi ${pin})`
: `Pi GPIO ${pin}`;
/** Raspberry Pi GPIO pin numbers. */

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

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