UNPKG

@robotical/sensors-dashboard

Version:

A dashboard tool for depicting Marty data by Robotical

396 lines (395 loc) 21 kB
"use strict"; require("core-js/modules/es.symbol.description.js"); require("core-js/modules/esnext.iterator.for-each.js"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; require("core-js/modules/web.dom-collections.iterator.js"); require("core-js/modules/es.regexp.exec.js"); require("core-js/modules/es.string.replace.js"); require("core-js/modules/es.number.to-fixed.js"); require("core-js/modules/esnext.iterator.constructor.js"); require("core-js/modules/esnext.iterator.filter.js"); require("core-js/modules/es.promise.js"); require("core-js/modules/esnext.iterator.map.js"); require("core-js/modules/web.url.js"); require("core-js/modules/web.url-search-params.js"); require("core-js/modules/es.array.reduce.js"); require("core-js/modules/esnext.iterator.reduce.js"); require("core-js/modules/esnext.iterator.some.js"); var _react = require("react"); var _MartyInterface = require("../../app-bridge/MartyInterface"); var _getAddonsList = _interopRequireDefault(require("../../utils/get-addons-list")); var _decider = require("../../utils/start-end-rules/decider"); var _startEndOptions = require("../../utils/start-end-rules/start-end-options"); var _AddonsList = _interopRequireDefault(require("../AddonsList")); var _Graph = _interopRequireDefault(require("../Graph")); var _GraphControls = _interopRequireDefault(require("../GraphControls")); var _stylesModule = _interopRequireDefault(require("./styles.module.css")); var _exportCsv = require("../../utils/export-csv"); var _fa = require("react-icons/fa"); var _Tooltip = _interopRequireDefault(require("@mui/material/Tooltip")); var _raft = require("@robotical/webapp-types/dist-types/src/types/raft"); var _CogInterface = _interopRequireDefault(require("../../app-bridge/CogInterface")); var _MockMartyInterface = _interopRequireDefault(require("../../app-bridge/mocks/MockMartyInterface")); var _MockCogInterface = _interopRequireDefault(require("../../app-bridge/mocks/MockCogInterface")); var _MockApplicationManager = require("../../mock/MockApplicationManager"); var _addonSelection = require("../../utils/addon-selection"); var _MicroBitInterface = _interopRequireDefault(require("../../app-bridge/MicroBitInterface")); var _MicroBitWebBluetooth = require("../../microbit/MicroBitWebBluetooth"); var _jsxRuntime = require("react/jsx-runtime"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } const hasAddonProvider = raftInterface => typeof raftInterface.getAvailableAddons === "function"; function GraphArea(_ref) { let { graphId, removeGraph, mainRef, raft, deviceName } = _ref; const [addons, setAddons] = (0, _react.useState)([]); const [, setRefreshGraphArea] = (0, _react.useState)(0); const [refreshAddons, setRefreshAddons] = (0, _react.useState)(0); const [refreshAddonsList, setRefreshAddonsList] = (0, _react.useState)(0); const [autoScrollEnabled, setAutoScrollEnabled] = (0, _react.useState)(false); const graphData = (0, _react.useRef)({}); const maxDataXValue = (0, _react.useRef)(0); const isTracking = (0, _react.useRef)(false); const hasSessionStarted = (0, _react.useRef)(false); const hasStartRuleMet = (0, _react.useRef)(false); const hasEndRuleMet = (0, _react.useRef)(false); const startSelectedOption = (0, _react.useRef)(undefined); const endSelectedOption = (0, _react.useRef)(undefined); const startOptions = (0, _react.useRef)([]); const endOptions = (0, _react.useRef)([]); const startDisplayingTime = (0, _react.useRef)(null); const [raftInterface, setRaftInterface] = (0, _react.useState)(null); const chartPanelRef = (0, _react.useRef)(null); const latestAddons = (0, _react.useRef)([]); const reactId = (0, _react.useId)(); const statusId = "graph-status-".concat(reactId.replace(/:/g, "")); const csvHintId = "graph-csv-hint-".concat(reactId.replace(/:/g, "")); (0, _react.useEffect)(() => { let interfaceInstance = null; if ((0, _MicroBitWebBluetooth.isMicroBitDevice)(raft)) { interfaceInstance = new _MicroBitInterface.default(raft); } else if ((0, _MockApplicationManager.isMockRaft)(raft)) { if (raft.type === _raft.RaftTypeE.COG) { const mockCog = new _MockCogInterface.default(); mockCog.start(); interfaceInstance = mockCog; } else { const mockMarty = new _MockMartyInterface.default(); mockMarty.start(); interfaceInstance = mockMarty; } } else if (raft.type === _raft.RaftTypeE.COG) { interfaceInstance = new _CogInterface.default(raft); } else if (raft.type === _raft.RaftTypeE.MARTY) { interfaceInstance = new _MartyInterface.MartyInterface(raft); } setRaftInterface(interfaceInstance); return () => { var _interfaceInstance; (_interfaceInstance = interfaceInstance) === null || _interfaceInstance === void 0 ? void 0 : _interfaceInstance.unsubscribeFromPublishedData(); }; }, [raft]); const cleanupStartEndRules = (0, _react.useCallback)(() => { var _startSelectedOption$, _startSelectedOption$2, _endSelectedOption$cu, _endSelectedOption$cu2; (_startSelectedOption$ = startSelectedOption.current) === null || _startSelectedOption$ === void 0 ? void 0 : (_startSelectedOption$2 = _startSelectedOption$[4]) === null || _startSelectedOption$2 === void 0 ? void 0 : _startSelectedOption$2.call(_startSelectedOption$); // cleanup rule (_endSelectedOption$cu = endSelectedOption.current) === null || _endSelectedOption$cu === void 0 ? void 0 : (_endSelectedOption$cu2 = _endSelectedOption$cu[4]) === null || _endSelectedOption$cu2 === void 0 ? void 0 : _endSelectedOption$cu2.call(_endSelectedOption$cu); // cleanup rule }, []); const onSelectAddon = (0, _react.useCallback)(() => { if (!hasSelectedSignal(latestAddons.current)) { isTracking.current = false; } setRefreshAddons(oldVal => oldVal + 1); }, []); const addSelectedListener = (0, _react.useCallback)(addonsArr => { for (const addon of addonsArr) { for (const addonInput of addon.addonInputs) { addonInput.setSelectedListener(onSelectAddon); } } }, [onSelectAddon]); const onAddonChange = (0, _react.useCallback)(evt => { /* for the following if's, order matters */ if (!isTracking.current) return; if (!startSelectedOption.current || !endSelectedOption.current) return; // decide whether or not to start based on start/end rules if (!hasStartRuleMet.current && !(0, _decider.isRuleMet)(evt, startSelectedOption.current)) return; hasStartRuleMet.current = true; if (hasEndRuleMet.current || (0, _decider.isRuleMet)(evt, endSelectedOption.current)) { hasEndRuleMet.current = true; isTracking.current = false; setRefreshGraphArea(old => old + 1); return; } if (!startDisplayingTime.current) { startDisplayingTime.current = new Date().getTime(); } maxDataXValue.current = getMaxDataXValue(graphData.current); const traceId = "".concat(evt.whoAmI, "=>").concat(evt.addonInput); const newTimestamp = (new Date().getTime() - startDisplayingTime.current) / 1000; if (graphData.current.hasOwnProperty(traceId)) { graphData.current[traceId].x.push(+newTimestamp.toFixed(2)); graphData.current[traceId].y.push(evt.value); } else { graphData.current[traceId] = { x: [+newTimestamp.toFixed(2)], y: [evt.value] }; } setRefreshGraphArea(old => old + 1); }, []); const addAddonsListeners = (0, _react.useCallback)(addns => { for (const addon of addns) { for (const addonInput of addon.addonInputs) { if (addonInput.selected) { raftInterface === null || raftInterface === void 0 ? void 0 : raftInterface.addEventListener("on".concat(addon.whoAmI + "=>" + addonInput.name, "Change"), graphId, onAddonChange); } else { raftInterface === null || raftInterface === void 0 ? void 0 : raftInterface.removeEventListener("on".concat(addon.whoAmI + "=>" + addonInput.name, "Change"), graphId, onAddonChange); } } } }, [raftInterface, graphId, onAddonChange]); const removeAddonsListeners = (0, _react.useCallback)(addns => { for (const addon of addns) { for (const addonInput of addon.addonInputs) { raftInterface === null || raftInterface === void 0 ? void 0 : raftInterface.removeEventListener("on".concat(addon.whoAmI + "=>" + addonInput.name, "Change"), graphId, onAddonChange); } } }, [raftInterface, graphId, onAddonChange]); const onAddonsChange = (0, _react.useCallback)(() => { setRefreshAddons(oldV => oldV + 1); setRefreshAddonsList(oldV => oldV + 1); }, []); (0, _react.useEffect)(() => { raftInterface === null || raftInterface === void 0 ? void 0 : raftInterface.addEventListener("onAddonsChange", "", onAddonsChange); return () => { raftInterface === null || raftInterface === void 0 ? void 0 : raftInterface.removeEventListener("onAddonsChange", "", onAddonsChange); }; }, [raftInterface, onAddonsChange]); (0, _react.useEffect)(() => { if (!raftInterface) return; const normalisedAddons = hasAddonProvider(raftInterface) ? raftInterface.getAvailableAddons() : (0, _getAddonsList.default)(raft); const addonsWithSelections = (0, _addonSelection.preserveAddonSelections)(normalisedAddons, latestAddons.current); addSelectedListener(addonsWithSelections); latestAddons.current = addonsWithSelections; setAddons(addonsWithSelections); }, [addSelectedListener, refreshAddonsList, raftInterface, raft]); (0, _react.useEffect)(() => { removeAddonsListeners(addons); addAddonsListeners(addons); const startOpts = (0, _startEndOptions.getRuleOptions)(addons, "start"); startOptions.current = startOpts; startSelectedOption.current = startOpts.filter(opt => opt[1] === "default")[0]; const endOpts = (0, _startEndOptions.getRuleOptions)(addons, "end"); endOptions.current = endOpts; endSelectedOption.current = endOpts.filter(opt => opt[1] === "default")[0]; setRefreshGraphArea(oldV => oldV + 1); return () => { removeAddonsListeners(addons); cleanupStartEndRules(); }; }, [addAddonsListeners, addons, cleanupStartEndRules, refreshAddons, removeAddonsListeners]); const stopHandler = (0, _react.useCallback)(() => { isTracking.current = false; hasSessionStarted.current = false; for (const traceKey in graphData.current) { delete graphData.current[traceKey]; } maxDataXValue.current = 0; hasStartRuleMet.current = false; cleanupStartEndRules(); hasEndRuleMet.current = false; startDisplayingTime.current = null; setRefreshGraphArea(old => old + 1); }, [cleanupStartEndRules]); const setIsTracking = (0, _react.useCallback)(value => { if (value && (!hasSelectedSignal(addons) || hasEndRuleMet.current)) return; if (value) { hasSessionStarted.current = true; } isTracking.current = value; setRefreshGraphArea(old => old + 1); }, [addons]); const startHandler = (0, _react.useCallback)(() => { setIsTracking(true); }, [setIsTracking]); const pauseHandler = (0, _react.useCallback)(() => { setIsTracking(false); }, [setIsTracking]); const autoScrollHandler = (0, _react.useCallback)(() => { setAutoScrollEnabled(oldValue => !oldValue); }, []); const onRuleOptionChange = (0, _react.useCallback)((selectedOption, rule) => { if (isTracking.current) return; // changing rule won't affect graph if it's currently traking cleanupStartEndRules(); if (rule === "start") { startSelectedOption.current = selectedOption; } else { endSelectedOption.current = selectedOption; } setRefreshGraphArea(old => old + 1); }, [cleanupStartEndRules]); const exportCsvHandler = (0, _react.useCallback)(async () => { const preparedCsvData = await (0, _exportCsv.prepareCSVData)(graphData.current); const titles = (0, _exportCsv.prepareTitles)(graphData.current, "Time (s)"); const csvContent = [titles, ...preparedCsvData].map(row => row.map(value => "\"".concat(String(value).replace(/"/g, '""'), "\"")).join(",")).join("\n"); const blob = new Blob(["\uFEFF", csvContent], { type: "text/csv;charset=utf-8" }); const url = window.URL.createObjectURL(blob); const downloadLink = document.createElement("a"); downloadLink.href = url; downloadLink.download = (0, _exportCsv.getCSVTitle)(graphData.current); document.body.appendChild(downloadLink); downloadLink.click(); downloadLink.remove(); window.setTimeout(() => window.URL.revokeObjectURL(url), 0); }, []); const currentStartSelectedOption = startSelectedOption.current; const currentEndSelectedOption = endSelectedOption.current; const currentStartOptions = startOptions.current; const currentEndOptions = endOptions.current; const selectedSignalCount = getSelectedSignalCount(addons); const hasData = hasGraphData(graphData.current); const recordingState = selectedSignalCount === 0 ? "choose-signals" : isTracking.current ? "recording" : hasSessionStarted.current ? "paused" : "ready"; const statusLabel = { "choose-signals": "Choose signals", ready: "Ready", recording: "Recording", paused: "Paused" }[recordingState]; const statusBadgeClassName = [_stylesModule.default.statusBadge, recordingState === "recording" ? _stylesModule.default.statusLive : recordingState === "ready" ? _stylesModule.default.statusReady : recordingState === "paused" ? _stylesModule.default.statusPaused : _stylesModule.default.statusChoose].join(" "); const emptyChartMessage = recordingState === "choose-signals" ? "Choose at least one signal to begin." : recordingState === "ready" ? "Ready when you are. Press Record to start plotting data." : recordingState === "recording" ? "Waiting for the first sensor reading…" : "No readings have been captured yet. Resume when you’re ready."; const graphControlsProps = { onClickPlay: startHandler, onClickPause: pauseHandler, onClickStop: stopHandler, onAutoScrollToggle: autoScrollHandler, autoScrollEnabled, onStartOptionChange: onRuleOptionChange, onEndOptionChange: onRuleOptionChange, startSelectedOption: currentStartSelectedOption, endSelectedOption: currentEndSelectedOption, startOptions: currentStartOptions, endOptions: currentEndOptions, recordingState, hasData, endConditionReached: hasEndRuleMet.current }; return /*#__PURE__*/(0, _jsxRuntime.jsxs)("section", { className: _stylesModule.default.graphCard, "aria-label": "Sensor graph for ".concat(deviceName), children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)("header", { className: _stylesModule.default.graphHeader, children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)("div", { className: _stylesModule.default.graphHeading, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("p", { className: _stylesModule.default.graphEyebrow, children: "Live data" }), /*#__PURE__*/(0, _jsxRuntime.jsxs)("h2", { className: _stylesModule.default.graphTitle, children: ["Sensor graph \u2014 ", deviceName] })] }), /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", { className: _stylesModule.default.graphHeaderActions, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("span", { className: statusBadgeClassName, id: statusId, children: statusLabel }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_Tooltip.default, { title: hasData ? "Export recorded data to CSV" : "Record some data before exporting a CSV", children: /*#__PURE__*/(0, _jsxRuntime.jsx)("span", { className: _stylesModule.default.iconButtonWrapper, children: /*#__PURE__*/(0, _jsxRuntime.jsxs)("button", { type: "button", onClick: exportCsvHandler, className: _stylesModule.default.iconButton, disabled: !hasData, "aria-describedby": !hasData ? csvHintId : undefined, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_fa.FaFileCsv, { "aria-hidden": "true" }), /*#__PURE__*/(0, _jsxRuntime.jsx)("span", { children: "CSV" })] }) }) }), !hasData && /*#__PURE__*/(0, _jsxRuntime.jsx)("span", { className: _stylesModule.default.visuallyHidden, id: csvHintId, children: "CSV export becomes available after sensor data has been recorded." }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_Tooltip.default, { title: "Close graph", children: /*#__PURE__*/(0, _jsxRuntime.jsx)("button", { type: "button", className: _stylesModule.default.iconButton, onClick: () => removeGraph(graphId), "aria-label": "Close graph", children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_fa.FaTimes, { "aria-hidden": "true" }) }) })] })] }), /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", { className: _stylesModule.default.graphBody, children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)("div", { className: _stylesModule.default.chartPanel, ref: chartPanelRef, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_GraphControls.default, _objectSpread(_objectSpread({}, graphControlsProps), {}, { mode: "primary" })), /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", { className: _stylesModule.default.chartFrame, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_Graph.default, { mainRef: mainRef, data: graphData.current, maxDataXValue: maxDataXValue.current, autoScrollEnabled: autoScrollEnabled, containerRef: chartPanelRef }), !hasData && /*#__PURE__*/(0, _jsxRuntime.jsx)("p", { className: _stylesModule.default.emptyChartMessage, children: emptyChartMessage })] }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_GraphControls.default, _objectSpread(_objectSpread({}, graphControlsProps), {}, { mode: "advanced" }))] }), /*#__PURE__*/(0, _jsxRuntime.jsxs)("aside", { className: _stylesModule.default.sidebar, "aria-labelledby": "".concat(statusId, "-signals"), children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)("div", { className: _stylesModule.default.sidebarHeader, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("h3", { id: "".concat(statusId, "-signals"), children: "Select signals" }), /*#__PURE__*/(0, _jsxRuntime.jsx)("p", { children: "Choose the data streams you want to visualise in this graph." })] }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_AddonsList.default, { addons: addons })] })] })] }); } const getMaxDataXValue = data => { let mxDataXValue = 1; for (const traceKey in data) { const currLen = data[traceKey].x.length; if (data[traceKey].x[currLen - 1] > mxDataXValue) mxDataXValue = data[traceKey].x[currLen - 1]; } return mxDataXValue; }; const getSelectedSignalCount = addons => addons.reduce((count, addon) => count + addon.addonInputs.reduce((addonCount, input) => addonCount + (input.selected ? 1 : 0), 0), 0); const hasSelectedSignal = addons => getSelectedSignalCount(addons) > 0; const hasGraphData = data => Object.values(data).some(trace => trace.x.length > 0); var _default = exports.default = GraphArea;