From d6b7d096535682a89a9455d999490b469995fdcf Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Wed, 29 Aug 2018 17:55:25 -0700 Subject: [PATCH 1/5] add motor plot --- ..._add_show_motor_plot_to_web_app_configs.rb | 9 + webpack/config/legacy_shims.ts | 3 +- webpack/config_storage/actions.ts | 7 +- webpack/config_storage/web_app_configs.ts | 4 +- .../__tests__/motor_position_plot_test.tsx | 51 +++++ webpack/controls/move/__tests__/move_test.tsx | 8 +- .../move/__tests__/settings_menu_test.tsx | 17 +- webpack/controls/move/motor_position_plot.tsx | 186 ++++++++++++++++++ webpack/controls/move/move.tsx | 13 +- webpack/controls/move/settings_menu.tsx | 50 ++--- webpack/css/global.scss | 8 + webpack/session_keys.ts | 1 + 12 files changed, 324 insertions(+), 33 deletions(-) create mode 100644 db/migrate/20180829211322_add_show_motor_plot_to_web_app_configs.rb create mode 100644 webpack/controls/move/__tests__/motor_position_plot_test.tsx create mode 100644 webpack/controls/move/motor_position_plot.tsx diff --git a/db/migrate/20180829211322_add_show_motor_plot_to_web_app_configs.rb b/db/migrate/20180829211322_add_show_motor_plot_to_web_app_configs.rb new file mode 100644 index 000000000..e9646a1a0 --- /dev/null +++ b/db/migrate/20180829211322_add_show_motor_plot_to_web_app_configs.rb @@ -0,0 +1,9 @@ +class AddShowMotorPlotToWebAppConfigs < ActiveRecord::Migration[5.2] + safety_assured + def change + add_column :web_app_configs, + :show_motor_plot, + :boolean, + default: false + end +end diff --git a/webpack/config/legacy_shims.ts b/webpack/config/legacy_shims.ts index 836a3000a..d01d4f9fb 100644 --- a/webpack/config/legacy_shims.ts +++ b/webpack/config/legacy_shims.ts @@ -20,7 +20,8 @@ import { getWebAppConfig } from "../resources/selectors_by_kind"; /** Avoid using this function in new places. Pass props instead. */ export function getBoolViaRedux(key: BooleanConfigKey): boolean | undefined { const conf = getWebAppConfig(store.getState().resources.index); - return conf && conf.body[key]; + // tslint:disable-next-line:no-any + return conf && (conf.body as any)[key]; } /** Avoid using this function in new places. Pass props instead. */ diff --git a/webpack/config_storage/actions.ts b/webpack/config_storage/actions.ts index 0211ea9a8..9dd2c0908 100644 --- a/webpack/config_storage/actions.ts +++ b/webpack/config_storage/actions.ts @@ -1,7 +1,8 @@ import { BooleanConfigKey as BooleanWebAppConfigKey, NumberConfigKey as NumberWebAppConfigKey, - StringConfigKey as StringWebAppConfigKey + StringConfigKey as StringWebAppConfigKey, + WebAppConfig } from "./web_app_configs"; import { GetState } from "../redux/interfaces"; import { edit, save } from "../api/crud"; @@ -12,7 +13,7 @@ export function toggleWebAppBool(key: BooleanWebAppConfigKey) { return (dispatch: Function, getState: GetState) => { const conf = getWebAppConfig(getState().resources.index); if (conf) { - const val = !conf.body[key]; + const val = !(conf.body as WebAppConfig)[key]; dispatch(edit(conf, { [key]: val })); dispatch(save(conf.uuid)); } else { @@ -33,7 +34,7 @@ export type GetWebAppConfigValue = (k: WebAppConfigKey) => WebAppConfigValue; export function getWebAppConfigValue(getState: GetState) { return (key: WebAppConfigKey): WebAppConfigValue => { const conf = getWebAppConfig(getState().resources.index); - return conf && conf.body[key]; + return conf && (conf.body as WebAppConfig)[key]; }; } diff --git a/webpack/config_storage/web_app_configs.ts b/webpack/config_storage/web_app_configs.ts index 38f04529c..f839c5762 100644 --- a/webpack/config_storage/web_app_configs.ts +++ b/webpack/config_storage/web_app_configs.ts @@ -46,6 +46,7 @@ export interface WebAppConfig { discard_unsaved: boolean; xy_swap: boolean; home_button_homing: boolean; + show_motor_plot: boolean; } export type NumberConfigKey = "id" @@ -89,4 +90,5 @@ export type BooleanConfigKey = "confirm_step_deletion" |"show_images" |"discard_unsaved" |"xy_swap" - |"home_button_homing"; + |"home_button_homing" + |"show_motor_plot"; diff --git a/webpack/controls/move/__tests__/motor_position_plot_test.tsx b/webpack/controls/move/__tests__/motor_position_plot_test.tsx new file mode 100644 index 000000000..e53e1a8fa --- /dev/null +++ b/webpack/controls/move/__tests__/motor_position_plot_test.tsx @@ -0,0 +1,51 @@ +jest.mock("moment", () => () => ({ unix: () => 1020 })); + +import * as React from "react"; +import { mount } from "enzyme"; +import { MotorPositionPlot, MotorPositionHistory } from "../motor_position_plot"; +import { BotLocationData, BotPosition } from "../../../devices/interfaces"; + +describe("", () => { + const fakePosition = (): BotPosition => ({ x: 0, y: 0, z: 0 }); + const fakeLocationData = (): BotLocationData => ({ + position: fakePosition(), + scaled_encoders: fakePosition(), + raw_encoders: fakePosition() + }); + + const fakeProps = () => ({ + locationData: fakeLocationData(), + }); + + it("renders", () => { + const wrapper = mount(); + ["x", "y", "z", "position", "seconds ago", "120", "100"].map(string => + expect(wrapper.text().toLowerCase()).toContain(string)); + }); + + it("renders motor position", () => { + const location1 = fakeLocationData(); + const location2 = fakeLocationData(); + location2.position.x = 100; + sessionStorage.setItem(MotorPositionHistory.array, JSON.stringify([ + { timestamp: 1000, locationData: location1 }, + { timestamp: 1010, locationData: location2 }, + ])); + const wrapper = mount(); + expect(wrapper.html()).toContain("M 120,0 L 120,0 L 110,-12.5 L 100,0"); + expect(wrapper.html()).toContain("M 120,0 L 120,0 L 110,0 L 100,0"); + }); + + it("handles undefined data", () => { + const location1 = fakeLocationData(); + const location2 = fakeLocationData(); + location2.position.x = undefined; + sessionStorage.setItem(MotorPositionHistory.array, JSON.stringify([ + { timestamp: 1000, locationData: location1 }, + { timestamp: 1010, locationData: location2 }, + ])); + const wrapper = mount(); + expect(wrapper.html()).not.toContain("M 120,0 L 120,0 L 110,-12.5 L 100,0"); + expect(wrapper.html()).toContain("M 120,0 L 120,0 L 110,0 L 100,0"); + }); +}); diff --git a/webpack/controls/move/__tests__/move_test.tsx b/webpack/controls/move/__tests__/move_test.tsx index a02722d54..ea5409949 100644 --- a/webpack/controls/move/__tests__/move_test.tsx +++ b/webpack/controls/move/__tests__/move_test.tsx @@ -13,7 +13,7 @@ jest.mock("../../../config_storage/actions", () => { }); import * as React from "react"; -import { mount } from "enzyme"; +import { mount, shallow } from "enzyme"; import { Move } from "../move"; import { bot } from "../../../__test_support__/fake_state/bot"; import { MoveProps } from "../interfaces"; @@ -81,4 +81,10 @@ describe("", () => { payload: 1 }); }); + + it("displays motor position plot", () => { + mockConfig.show_motor_plot = true; + const wrapper = shallow(); + expect(wrapper.html()).toContain("motor-position-plot"); + }); }); diff --git a/webpack/controls/move/__tests__/settings_menu_test.tsx b/webpack/controls/move/__tests__/settings_menu_test.tsx index f2e2c80c3..de106752f 100644 --- a/webpack/controls/move/__tests__/settings_menu_test.tsx +++ b/webpack/controls/move/__tests__/settings_menu_test.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { mount } from "enzyme"; import { BooleanSetting } from "../../../session_keys"; -import { moveWidgetSetting } from "../settings_menu"; +import { moveWidgetSetting, MoveWidgetSettingsMenu } from "../settings_menu"; describe("moveWidgetSetting()", () => { it("renders setting", () => { @@ -13,3 +13,18 @@ describe("moveWidgetSetting()", () => { expect(wrapper.text().toLowerCase()).toContain(string)); }); }); + +describe("", () => { + const fakeProps = () => ({ + toggle: jest.fn(), + getValue: jest.fn(), + }); + + it("displays motor plot toggle", () => { + const noToggle = mount(); + expect(noToggle.text()).not.toContain("Motor position plot"); + localStorage.setItem("FUTURE_FEATURES", "true"); + const wrapper = mount(); + expect(wrapper.text()).toContain("Motor position plot"); + }); +}); diff --git a/webpack/controls/move/motor_position_plot.tsx b/webpack/controls/move/motor_position_plot.tsx new file mode 100644 index 000000000..2e2f59278 --- /dev/null +++ b/webpack/controls/move/motor_position_plot.tsx @@ -0,0 +1,186 @@ +import * as React from "react"; +import * as _ from "lodash"; +import { Xyz, LocationName, Dictionary } from "farmbot"; +import * as moment from "moment"; +import { BotLocationData, BotPosition } from "../../devices/interfaces"; +import { trim } from "../../util"; +import { t } from "i18next"; + +const HEIGHT = 50; +const HISTORY_LENGTH_SECONDS = 120; +const BORDER_WIDTH = 15; +const BORDERS = BORDER_WIDTH * 2; +const MAX_X = HISTORY_LENGTH_SECONDS; +const DEFAULT_Y_MAX = 100; + +const COLOR_LOOKUP: Dictionary = { + x: "red", y: "green", z: "blue" +}; +const LINEWIDTH_LOOKUP: Dictionary = { + position: 0.5, scaled_encoders: 0.25 +}; + +export enum MotorPositionHistory { + array = "motorPositionHistoryArray", +} + +type Entry = { + timestamp: number, + locationData: Record +}; + +type Paths = Record>; + +const getArray = (): Entry[] => + JSON.parse(_.get(sessionStorage, MotorPositionHistory.array, "[]")); + +const getReversedArray = (): Entry[] => _.cloneDeep(getArray()).reverse(); + +const getLastEntry = (): Entry | undefined => { + const array = getArray(); + return array[array.length - 1]; +}; + +const findYLimit = (): number => { + const array = getArray(); + const arrayAbsMax = _.max(array.map(entry => + _.max(["position", "scaled_encoders"].map((name: LocationName) => + _.max(["x", "y", "z"].map((axis: Xyz) => + Math.abs(entry.locationData[name][axis] || 0) + 1)))))); + return Math.max(_.ceil(arrayAbsMax || 0, -2), DEFAULT_Y_MAX); +}; + +const updateArray = (update: Entry): Entry[] => { + const arr = getArray(); + const last = getLastEntry(); + if (update && _.isNumber(update.locationData.position.x) && + (!last || !_.isEqual(last.timestamp, update.timestamp))) { + arr.push(update); + } + const newArray = _.takeRight(arr, 100) + .filter(x => { + const entryAge = (last ? last.timestamp : moment().unix()) - x.timestamp; + return entryAge <= HISTORY_LENGTH_SECONDS; + }); + sessionStorage.setItem(MotorPositionHistory.array, JSON.stringify(newArray)); + return newArray; +}; + +const newPaths = (): Paths => ({ + position: { x: "", y: "", z: "" }, + scaled_encoders: { x: "", y: "", z: "" }, + raw_encoders: { x: "", y: "", z: "" } +}); + +const getPaths = (): Paths => { + const last = getLastEntry(); + const maxY = findYLimit(); + const paths = newPaths(); + if (last) { + getReversedArray().map(entry => { + ["position", "scaled_encoders"].map((name: LocationName) => { + ["x", "y", "z"].map((axis: Xyz) => { + const lastPos = last.locationData[name][axis]; + const pos = entry.locationData[name][axis]; + if (_.isNumber(lastPos) && _.isFinite(lastPos) + && _.isNumber(maxY) && _.isNumber(pos)) { + if (!paths[name][axis].startsWith("M")) { + const yStart = -lastPos / maxY * HEIGHT / 2; + paths[name][axis] = `M ${MAX_X},${yStart} `; + } + const x = MAX_X - (last.timestamp - entry.timestamp); + const y = -pos / maxY * HEIGHT / 2; + paths[name][axis] += `L ${x},${y} `; + } + }); + }); + }); + } + return paths; +}; + +const TitleLegend = () => { + const titleY = -(HEIGHT + BORDER_WIDTH) / 2; + const legendX = HISTORY_LENGTH_SECONDS / 4; + return + {"X"} + {"Y"} + {"Z"} + {t("Position (mm)")} + ; +}; + +const YAxisLabels = () => { + const maxY = findYLimit(); + return + {[maxY, maxY / 2, 0, -maxY / 2, -maxY].map(yPosition => + + + {yPosition} + + + {yPosition} + + )} + ; +}; + +const XAxisLabels = () => + + + {t("seconds ago")} + + {_.range(0, HISTORY_LENGTH_SECONDS + 1, 20).map(secondsAgo => + + {secondsAgo} + )} + ; + +const PlotBackground = () => + + + + ; + +const PlotLines = ({ locationData }: { locationData: BotLocationData }) => { + updateArray({ timestamp: moment().unix(), locationData }); + const paths = getPaths(); + return + {["position", "scaled_encoders"].map((name: LocationName) => + ["x", "y", "z"].map((axis: Xyz) => + ))} + ; +}; + +export const MotorPositionPlot = (props: { locationData: BotLocationData }) => { + return + + + + + + + + ; +}; diff --git a/webpack/controls/move/move.tsx b/webpack/controls/move/move.tsx index 4913ba7f0..b1fb10c8e 100644 --- a/webpack/controls/move/move.tsx +++ b/webpack/controls/move/move.tsx @@ -14,6 +14,8 @@ import { MoveProps } from "./interfaces"; import { MoveWidgetSettingsMenu } from "./settings_menu"; import { JogControlsGroup } from "./jog_controls_group"; import { BotPositionRows } from "./bot_position_rows"; +import { MotorPositionPlot } from "./motor_position_plot"; +import { Popover, Position } from "@blueprintjs/core"; export class Move extends React.Component { @@ -30,9 +32,12 @@ export class Move extends React.Component { - + + + + @@ -55,6 +60,8 @@ export class Move extends React.Component { arduinoBusy={this.props.arduinoBusy} firmware_version={informational_settings.firmware_version} /> + {this.props.getWebAppConfigVal(BooleanSetting.show_motor_plot) && + } ; } diff --git a/webpack/controls/move/settings_menu.tsx b/webpack/controls/move/settings_menu.tsx index dd5c706a9..beb44e9f1 100644 --- a/webpack/controls/move/settings_menu.tsx +++ b/webpack/controls/move/settings_menu.tsx @@ -1,6 +1,5 @@ import * as React from "react"; import { t } from "i18next"; -import { Popover, Position } from "@blueprintjs/core"; import { BooleanConfigKey as BooleanWebAppConfigKey } from "../../config_storage/web_app_configs"; @@ -24,29 +23,34 @@ export const MoveWidgetSettingsMenu = ({ toggle, getValue }: { getValue: GetWebAppBool }) => { const Setting = moveWidgetSetting(toggle, getValue); - return - -
-

{t("Invert Jog Buttons")}

- - - + return
+

{t("Invert Jog Buttons")}

+ + + -

{t("Display Encoder Data")}

- - +

{t("Display Encoder Data")}

+ + -

{t("Swap jog buttons (and rotate map)")}

- +

{t("Swap jog buttons (and rotate map)")}

+ -

{t("Home button behavior")}

- -
- ; +

{t("Home button behavior")}

+ + + {localStorage.getItem("FUTURE_FEATURES") && +
+

{t("Motor position plot")}

+ +
} +
; }; diff --git a/webpack/css/global.scss b/webpack/css/global.scss index c454bc036..1e1bfdaa2 100644 --- a/webpack/css/global.scss +++ b/webpack/css/global.scss @@ -607,6 +607,14 @@ ul { } } +.motor-position-plot-border { + text { + font-size: 0.4rem; + text-anchor: middle; + dominant-baseline: middle; + } +} + .controls-popup, .controls-popup-menu-outer { position: fixed; diff --git a/webpack/session_keys.ts b/webpack/session_keys.ts index 5f8ecc2e3..0bcaa8f81 100644 --- a/webpack/session_keys.ts +++ b/webpack/session_keys.ts @@ -14,6 +14,7 @@ export const BooleanSetting: Record = { show_images: "show_images", xy_swap: "xy_swap", home_button_homing: "home_button_homing", + show_motor_plot: "show_motor_plot", /** "Labs" feature names. (App preferences) */ stub_config: "stub_config", From 716ebdd293703cdc07e6b504b28ed0ee4b3e8a93 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Wed, 29 Aug 2018 18:08:34 -0700 Subject: [PATCH 2/5] hide for now --- webpack/farm_designer/plants/plant_panel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack/farm_designer/plants/plant_panel.tsx b/webpack/farm_designer/plants/plant_panel.tsx index 09320082c..7e54b3022 100644 --- a/webpack/farm_designer/plants/plant_panel.tsx +++ b/webpack/farm_designer/plants/plant_panel.tsx @@ -113,7 +113,7 @@ export function PlantPanel(props: PlantPanelProps) {