UNPKG

@robotical/sensors-dashboard

Version:

A dashboard tool for depicting Marty data by Robotical

458 lines (457 loc) 21.7 kB
"use strict"; require("core-js/modules/es.weak-map.js"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; require("core-js/modules/web.dom-collections.iterator.js"); require("core-js/modules/esnext.iterator.constructor.js"); require("core-js/modules/esnext.iterator.filter.js"); require("core-js/modules/esnext.iterator.for-each.js"); require("core-js/modules/es.promise.js"); require("core-js/modules/es.regexp.to-string.js"); require("core-js/modules/esnext.iterator.map.js"); var _react = require("react"); var _GraphArea = _interopRequireDefault(require("../GraphArea")); var _stylesModule = _interopRequireDefault(require("./styles.module.css")); var _fa = require("react-icons/fa"); var _microbitSmall = _interopRequireDefault(require("../../assets/connect-button/microbit-small.svg")); var _ModalState = _interopRequireDefault(require("../../state-observables/modal/ModalState")); var _NewGraphModal = _interopRequireDefault(require("../modals/NewGraphModal")); var _raftDisplayName = require("../../utils/raft-display-name"); var _MicroBitWebBluetooth = _interopRequireWildcard(require("../../microbit/MicroBitWebBluetooth")); var _jsxRuntime = require("react/jsx-runtime"); function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } function MainContent(_ref) { let { mainRef } = _ref; const [hasConnectedRafts, setHasConnectedRafts] = (0, _react.useState)(false); const [connectionPhase, setConnectionPhase] = (0, _react.useState)("idle"); const [connectionMessage, setConnectionMessage] = (0, _react.useState)(""); const [microBit, setMicroBit] = (0, _react.useState)(null); const [microBitPhase, setMicroBitPhase] = (0, _react.useState)("idle"); const [microBitMessage, setMicroBitMessage] = (0, _react.useState)(""); const [, triggerRerender] = (0, _react.useState)(0); const graphs = (0, _react.useRef)([]); const graphSubRefs = (0, _react.useRef)({}); const microBitRef = (0, _react.useRef)(null); const pendingMicroBitRef = (0, _react.useRef)(null); const microBitDisconnectCleanup = (0, _react.useRef)(null); const isMounted = (0, _react.useRef)(true); const isConnected = hasConnectedRafts || Boolean(microBit); const canConnectMicroBit = (0, _MicroBitWebBluetooth.isMicroBitWebBluetoothSupported)(); const removeGraph = graphId => { const cleanup = graphSubRefs.current[graphId]; if (cleanup) { cleanup(); delete graphSubRefs.current[graphId]; } const graphsUpdated = graphs.current.filter(graph => graph.graphId !== graphId); graphs.current = graphsUpdated; triggerRerender(old => old + 1); }; const syncConnectionState = (0, _react.useCallback)(() => { var _window$applicationMa; const connectedRafts = ((_window$applicationMa = window.applicationManager) === null || _window$applicationMa === void 0 ? void 0 : _window$applicationMa.connectedRafts) || {}; const connected = Object.keys(connectedRafts).length > 0; setHasConnectedRafts(connected); if (connected) { setConnectionPhase("idle"); setConnectionMessage(""); } return connected; }, []); (0, _react.useEffect)(() => { syncConnectionState(); const connectedRaftInterval = setInterval(() => { syncConnectionState(); }, 750); return () => { clearInterval(connectedRaftInterval); }; }, [syncConnectionState]); (0, _react.useEffect)(() => { isMounted.current = true; return () => { var _microBitDisconnectCl, _microBitRef$current, _pendingMicroBitRef$c; isMounted.current = false; Object.values(graphSubRefs.current).forEach(cleanup => cleanup()); graphSubRefs.current = {}; (_microBitDisconnectCl = microBitDisconnectCleanup.current) === null || _microBitDisconnectCl === void 0 ? void 0 : _microBitDisconnectCl.call(microBitDisconnectCleanup); microBitDisconnectCleanup.current = null; (_microBitRef$current = microBitRef.current) === null || _microBitRef$current === void 0 ? void 0 : _microBitRef$current.disconnect(); (_pendingMicroBitRef$c = pendingMicroBitRef.current) === null || _pendingMicroBitRef$c === void 0 ? void 0 : _pendingMicroBitRef$c.disconnect(); microBitRef.current = null; pendingMicroBitRef.current = null; }; }, []); const connectRobot = async () => { if (!window.applicationManager) { setConnectionPhase("error"); setConnectionMessage("Device connections are not available in this view."); return; } setConnectionPhase("connecting"); setConnectionMessage("Choose your device in the connection window."); try { await window.applicationManager.connectGeneric(() => { syncConnectionState(); }); const connected = syncConnectionState(); if (!connected) { setConnectionPhase("idle"); setConnectionMessage("No device was connected. You can try again when you are ready."); } } catch (error) { console.error("Unable to connect robot", error); setConnectionPhase("error"); setConnectionMessage("We could not connect to the device. Please try again."); } }; const connectMicroBit = async () => { var _microBitRef$current2; if (!(0, _MicroBitWebBluetooth.isMicroBitWebBluetoothSupported)()) { setMicroBitPhase("error"); setMicroBitMessage("Web Bluetooth is not available in this browser."); return; } if ((_microBitRef$current2 = microBitRef.current) !== null && _microBitRef$current2 !== void 0 && _microBitRef$current2.isConnected() || pendingMicroBitRef.current) { return; } const candidate = new _MicroBitWebBluetooth.default(); pendingMicroBitRef.current = candidate; setMicroBitPhase("connecting"); setMicroBitMessage("Choose your micro:bit in the Bluetooth window."); try { await candidate.connect(); if (!isMounted.current || pendingMicroBitRef.current !== candidate) { candidate.disconnect(); return; } if (!candidate.isConnected()) { throw new Error("The micro:bit disconnected while connecting."); } const unsubscribeDisconnect = candidate.addDisconnectListener(disconnectedMicroBit => { var _microBitDisconnectCl2; if (microBitRef.current !== disconnectedMicroBit) return; (_microBitDisconnectCl2 = microBitDisconnectCleanup.current) === null || _microBitDisconnectCl2 === void 0 ? void 0 : _microBitDisconnectCl2.call(microBitDisconnectCleanup); microBitDisconnectCleanup.current = null; microBitRef.current = null; if (isMounted.current) { setMicroBit(null); setMicroBitPhase("idle"); setMicroBitMessage("micro:bit disconnected."); } }); microBitDisconnectCleanup.current = unsubscribeDisconnect; microBitRef.current = candidate; setMicroBit(candidate); setMicroBitPhase("idle"); setMicroBitMessage(""); } catch (error) { if (!isMounted.current) return; const wasCancelled = (error === null || error === void 0 ? void 0 : error.name) === "NotFoundError"; setMicroBitPhase(wasCancelled ? "idle" : "error"); setMicroBitMessage(wasCancelled ? "No micro:bit was selected." : "Could not connect. Check that the micro:bit is powered on and running the Robotical firmware."); } finally { if (pendingMicroBitRef.current === candidate) { pendingMicroBitRef.current = null; } } }; const disconnectMicroBit = () => { var _microBitDisconnectCl3; const connectedMicroBit = microBitRef.current; if (!connectedMicroBit) return; (_microBitDisconnectCl3 = microBitDisconnectCleanup.current) === null || _microBitDisconnectCl3 === void 0 ? void 0 : _microBitDisconnectCl3.call(microBitDisconnectCleanup); microBitDisconnectCleanup.current = null; microBitRef.current = null; setMicroBit(null); setMicroBitPhase("idle"); setMicroBitMessage("micro:bit disconnected."); connectedMicroBit.disconnect(); }; const addGraphHandler = async () => { var _window$applicationMa2, _window$applicationMa3, _window$applicationMa4; const deviceId = await _ModalState.default.setModal(/*#__PURE__*/(0, _react.createElement)(_NewGraphModal.default, { microBit }), "Add new graph"); if (!deviceId) { return; } const device = (microBit === null || microBit === void 0 ? void 0 : microBit.id) === deviceId ? microBit : (_window$applicationMa2 = window.applicationManager) === null || _window$applicationMa2 === void 0 ? void 0 : (_window$applicationMa3 = _window$applicationMa2.connectedRafts) === null || _window$applicationMa3 === void 0 ? void 0 : _window$applicationMa3[deviceId]; if (!device) { return; } if ((0, _MicroBitWebBluetooth.isMicroBitDevice)(device) && !device.isConnected()) { setMicroBitMessage("The micro:bit disconnected before the graph was created."); return; } const deviceName = (0, _MicroBitWebBluetooth.isMicroBitDevice)(device) ? device.getFriendlyName() : await (0, _raftDisplayName.resolveRaftDisplayName)(device, ((_window$applicationMa4 = window.applicationManager) === null || _window$applicationMa4 === void 0 ? void 0 : _window$applicationMa4.connectedRaftsContext) || []); const graphsUpdated = [...graphs.current]; const GRAPH_ID = new Date().getTime().toString(); const disconnectCb = () => { removeGraph(GRAPH_ID); }; if ((0, _MicroBitWebBluetooth.isMicroBitDevice)(device)) { graphSubRefs.current[GRAPH_ID] = device.addDisconnectListener(disconnectCb); } else { const mgr = getOrCreateRaftDisconnectManager(deviceId, device); mgr.subscribe(disconnectCb); graphSubRefs.current[GRAPH_ID] = () => mgr.unsubscribe(disconnectCb); } graphsUpdated.push({ graphId: GRAPH_ID, element: /*#__PURE__*/(0, _jsxRuntime.jsx)(_GraphArea.default, { raft: device, deviceName: deviceName, mainRef: mainRef, graphId: GRAPH_ID, removeGraph: removeGraph }, GRAPH_ID) }); graphs.current = graphsUpdated; triggerRerender(old => old + 1); }; if (!isConnected) { return /*#__PURE__*/(0, _jsxRuntime.jsxs)("section", { className: _stylesModule.default.connectEmptyState, "aria-labelledby": "connect-empty-title", children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)("div", { className: _stylesModule.default.connectEmptyHero, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("div", { className: _stylesModule.default.emptyStateIcon, "aria-hidden": "true", children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_fa.FaPlug, {}) }), /*#__PURE__*/(0, _jsxRuntime.jsx)("p", { className: _stylesModule.default.emptyStateEyebrow, children: "Start here" }), /*#__PURE__*/(0, _jsxRuntime.jsx)("h2", { id: "connect-empty-title", children: "Connect a device to see its sensors" }), /*#__PURE__*/(0, _jsxRuntime.jsx)("p", { className: _stylesModule.default.emptyStateDescription, children: "Pair Marty, Cog, or a micro:bit, then choose the signals you want to turn into a live graph." }), /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", { className: _stylesModule.default.connectActions, children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)("button", { type: "button", className: _stylesModule.default.connectButton, onClick: connectRobot, disabled: connectionPhase === "connecting", children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_fa.FaPlug, { "aria-hidden": "true" }), /*#__PURE__*/(0, _jsxRuntime.jsx)("span", { children: connectionPhase === "connecting" ? "Connecting…" : "Connect Marty or Cog" })] }), /*#__PURE__*/(0, _jsxRuntime.jsxs)("button", { type: "button", className: "".concat(_stylesModule.default.connectButton, " ").concat(_stylesModule.default.secondaryButton), onClick: connectMicroBit, disabled: microBitPhase === "connecting", title: !canConnectMicroBit ? "Web Bluetooth is unavailable" : undefined, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("img", { src: _microbitSmall.default, alt: "", "aria-hidden": "true", className: _stylesModule.default.microBitButtonIcon }), /*#__PURE__*/(0, _jsxRuntime.jsx)("span", { children: microBitPhase === "connecting" ? "Connecting…" : "Connect micro:bit" })] })] }), connectionMessage && /*#__PURE__*/(0, _jsxRuntime.jsx)("p", { className: "".concat(_stylesModule.default.connectionFeedback, " ").concat(connectionPhase === "error" ? _stylesModule.default.connectionError : ""), role: connectionPhase === "error" ? "alert" : "status", "aria-live": "polite", children: connectionMessage }), microBitMessage && /*#__PURE__*/(0, _jsxRuntime.jsx)("p", { className: "".concat(_stylesModule.default.connectionFeedback, " ").concat(microBitPhase === "error" ? _stylesModule.default.connectionError : ""), role: microBitPhase === "error" ? "alert" : "status", "aria-live": "polite", children: microBitMessage })] }), /*#__PURE__*/(0, _jsxRuntime.jsxs)("ol", { className: _stylesModule.default.quickJourney, "aria-label": "Dashboard setup steps", children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)("li", { children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("span", { children: "1" }), /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", { children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("strong", { children: "Connect" }), /*#__PURE__*/(0, _jsxRuntime.jsx)("small", { children: "Pair Marty, Cog, or micro:bit." })] })] }), /*#__PURE__*/(0, _jsxRuntime.jsxs)("li", { children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("span", { children: "2" }), /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", { children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("strong", { children: "Add a graph" }), /*#__PURE__*/(0, _jsxRuntime.jsx)("small", { children: "Choose the connected device." })] })] }), /*#__PURE__*/(0, _jsxRuntime.jsxs)("li", { children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("span", { children: "3" }), /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", { children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("strong", { children: "Pick signals" }), /*#__PURE__*/(0, _jsxRuntime.jsx)("small", { children: "Start recording live data." })] })] })] })] }); } const hasGraphs = graphs.current.length > 0; return /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", { className: _stylesModule.default.mainContent, children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)("div", { className: _stylesModule.default.graphsToolbar, children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)("div", { children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)("p", { className: _stylesModule.default.toolbarEyebrow, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_fa.FaCheckCircle, { "aria-hidden": "true" }), " Device connected"] }), /*#__PURE__*/(0, _jsxRuntime.jsx)("h2", { children: "Graphs" }), /*#__PURE__*/(0, _jsxRuntime.jsx)("p", { children: hasGraphs ? "".concat(graphs.current.length, " live workspace").concat(graphs.current.length === 1 ? "" : "s") : "Create a graph to begin exploring sensor data." })] }), /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", { className: _stylesModule.default.toolbarActions, children: [microBit ? /*#__PURE__*/(0, _jsxRuntime.jsxs)("button", { type: "button", onClick: disconnectMicroBit, className: _stylesModule.default.deviceActionButton, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_fa.FaUnlink, { "aria-hidden": "true" }), /*#__PURE__*/(0, _jsxRuntime.jsxs)("span", { children: ["Disconnect ", microBit.getFriendlyName()] })] }) : /*#__PURE__*/(0, _jsxRuntime.jsxs)("button", { type: "button", onClick: connectMicroBit, className: _stylesModule.default.deviceActionButton, disabled: microBitPhase === "connecting", title: !canConnectMicroBit ? "Web Bluetooth is unavailable" : undefined, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("img", { src: _microbitSmall.default, alt: "", "aria-hidden": "true", className: _stylesModule.default.microBitButtonIcon }), /*#__PURE__*/(0, _jsxRuntime.jsx)("span", { children: microBitPhase === "connecting" ? "Connecting…" : "Connect micro:bit" })] }), /*#__PURE__*/(0, _jsxRuntime.jsxs)("button", { type: "button", onClick: addGraphHandler, className: _stylesModule.default.addGraphBtn, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_fa.FaPlus, { "aria-hidden": "true" }), /*#__PURE__*/(0, _jsxRuntime.jsx)("span", { children: "Add graph" }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_fa.FaChartLine, { "aria-hidden": "true" })] })] })] }), microBitMessage && /*#__PURE__*/(0, _jsxRuntime.jsx)("p", { className: "".concat(_stylesModule.default.toolbarFeedback, " ").concat(microBitPhase === "error" ? _stylesModule.default.connectionError : ""), role: microBitPhase === "error" ? "alert" : "status", "aria-live": "polite", children: microBitMessage }), /*#__PURE__*/(0, _jsxRuntime.jsx)("div", { className: _stylesModule.default.graphsArea, children: graphs.current.map(graphArea => { return graphArea.element; }) }), !hasGraphs && /*#__PURE__*/(0, _jsxRuntime.jsxs)("section", { className: _stylesModule.default.addNewGraphMessage, "aria-labelledby": "add-first-graph-title", children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_fa.FaChartLine, { "aria-hidden": "true" }), /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", { children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("p", { className: _stylesModule.default.emptyStateEyebrow, children: "Your workspace is ready" }), /*#__PURE__*/(0, _jsxRuntime.jsx)("h3", { id: "add-first-graph-title", children: "Build your first live graph" }), /*#__PURE__*/(0, _jsxRuntime.jsxs)("p", { children: ["Select ", /*#__PURE__*/(0, _jsxRuntime.jsx)("strong", { children: "Add graph" }), ", choose a device, then pick one or more signals."] })] })] })] }); } var _default = exports.default = MainContent; // One subscription per raft, with fan-out to all registered graph callbacks const raftManagers = {}; const getOrCreateRaftDisconnectManager = (raftId, raft) => { if (!raftManagers[raftId]) { const callbacks = new Set(); const observer = { notify(eventType, eventEnum) { switch (eventType) { case "conn": switch (eventEnum) { case 3: // BLE_DISCONNECTED console.log("Marty Disconnected (fan-out)!!!!!!!!!"); // Copy to avoid mutation during iteration const toCall = Array.from(callbacks); callbacks.clear(); toCall.forEach(cb => { try { cb(); } catch (e) { console.error(e); } }); // Tear down subscription for this raft raft.unsubscribe(observer); delete raftManagers[raftId]; break; default: break; } break; default: break; } } }; raft.subscribe(observer, ["conn"]); raftManagers[raftId] = { callbacks, observer, raft }; } return { subscribe: cb => { raftManagers[raftId].callbacks.add(cb); }, unsubscribe: cb => { const mgr = raftManagers[raftId]; if (!mgr) return; mgr.callbacks.delete(cb); if (mgr.callbacks.size === 0) { // No more listeners; clean up subscription mgr.raft.unsubscribe(mgr.observer); delete raftManagers[raftId]; } } }; };