@robotical/sensors-dashboard
Version:
A dashboard tool for depicting Marty data by Robotical
458 lines (457 loc) • 21.7 kB
JavaScript
"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];
}
}
};
};