From ed92226e5736c2ef41c5f405c6d9c7a73663941f Mon Sep 17 00:00:00 2001 From: Felix Erdmann Date: Mon, 25 Nov 2024 16:06:42 +0000 Subject: [PATCH] feat(simulator): elapsed simulation time --- package-lock.json | 30 +++-- src/components/CodeViewer.js | 49 ++------ src/components/Simulator/flow.js | 111 +++++++++++++++++ src/components/Simulator/index.js | 200 ++++++++++++++---------------- src/reducers/simulatorReducer.js | 4 + 5 files changed, 232 insertions(+), 162 deletions(-) create mode 100644 src/components/Simulator/flow.js diff --git a/package-lock.json b/package-lock.json index 083baba8..0cf0e229 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,9 +31,8 @@ "@testing-library/jest-dom": "^5.16.1", "@testing-library/react": "^12.1.2", "@testing-library/user-event": "^7.2.1", - "blockly": "^11.1.1", "@xyflow/react": "^12.3.5", - "axios": "^0.22.0", + "axios": "^0.28.0", "blockly": "^11.1.1", "file-saver": "^2.0.5", "js-interpreter": "^5.2.1", @@ -2216,7 +2215,19 @@ "node": ">=8.17.0" }, "peerDependencies": { - "blockly": "^9.0.0" + "blockly": "^11.0.0" + } + }, + "node_modules/@blockly/field-colour": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/@blockly/field-colour/-/field-colour-5.0.9.tgz", + "integrity": "sha512-ebj4oGwrqkcGKTYjLDHLQYilZ1qfmiPIfi3QVv/ROfSut4VPEf4JEnTmc/AJJrKQgzygdbz02QLZ2DBlVkoWNg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "blockly": "^11.0.0" } }, "node_modules/@blockly/field-grid-dropdown": { @@ -9505,19 +9516,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", diff --git a/src/components/CodeViewer.js b/src/components/CodeViewer.js index 33dd309b..dd529f85 100644 --- a/src/components/CodeViewer.js +++ b/src/components/CodeViewer.js @@ -6,14 +6,13 @@ import withStyles from "@mui/styles/withStyles"; import MuiAccordion from "@mui/material/Accordion"; import MuiAccordionSummary from "@mui/material/AccordionSummary"; import MuiAccordionDetails from "@mui/material/AccordionDetails"; -import IconButton from "@mui/material/IconButton"; -import { faPlay, faStop } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Card } from "@mui/material"; import * as Blockly from "blockly"; import { default as MonacoEditor } from "@monaco-editor/react"; -import { startSimulator, stopSimulator } from "../actions/simulatorActions"; -import SimulatorFlow from "./Simulator"; +import { faMicrochip } from "@fortawesome/free-solid-svg-icons"; + +import Simulator from "./Simulator"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; // FIXME checkout https://mui.com/components/use-media-query/#migrating-from-withwidth const withWidth = () => (WrappedComponent) => (props) => ( @@ -121,16 +120,16 @@ class CodeViewer extends Component { // onChange={this.onChange} > - {/* - Simulator Icon - */} -
Simulator
+ +
Simulator
+
- {this.props.isSimulatorRunning ? ( - - - - ) : ( - - - - )} - -
- - +
({ arduino: state.workspace.code.arduino, xml: state.workspace.code.xml, tooltip: state.workspace.code.tooltip, - simulator: state.simulator.code, - isSimulatorRunning: state.simulator.isRunning, - modules: state.simulator.modules, }); -export default connect(mapStateToProps, { startSimulator, stopSimulator })( - withWidth()(CodeViewer), -); +export default connect(mapStateToProps, null)(withWidth()(CodeViewer)); diff --git a/src/components/Simulator/flow.js b/src/components/Simulator/flow.js new file mode 100644 index 00000000..ac23e220 --- /dev/null +++ b/src/components/Simulator/flow.js @@ -0,0 +1,111 @@ +import { useCallback, useEffect, memo } from "react"; +import { + ReactFlow, + Node, + Edge, + Background, + Controls, + MiniMap, + useNodesState, + useEdgesState, + addEdge, + Connection, +} from "@xyflow/react"; +import SenseBoxMCUS2 from "./nodes/mcu-s2"; +import "@xyflow/react/dist/style.css"; +import HDC1080 from "./nodes/hdc1080"; +import Display from "./nodes/display"; + +const nodeTypes = { + board: SenseBoxMCUS2, + senseBox_hdc1080: HDC1080, + senseBox_display: Display, +}; + +const initialNodes = [ + { + id: "b_0", + type: "board", + position: { x: 400, y: 100 }, + }, +]; + +const initialEdges = [ + // { id: "e1-2", source: "1", target: "2" }, + // { id: "e1-3", source: "1", target: "3" }, + // { id: "e1-4", source: "1", target: "4" }, + // { id: "e3-5", source: "3", target: "5" }, + // { id: "e2-5", source: "2", target: "5" }, + // { id: "e4-5", source: "4", target: "5" }, +]; + +const SimulatorFlow = (props) => { + const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); + const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + + useEffect(() => { + // calculate new edges + const newEdges = []; + nodes.forEach((node) => { + if (node.type === "board") { + props.modules.forEach((module, index) => { + newEdges.push({ + id: `e${node.id}-${index}`, + source: node.id, + target: `m_${index}`, + }); + }); + } + }); + + setEdges([...initialEdges, ...newEdges]); + }, [nodes]); + + useEffect(() => { + const newNodes = props.modules + .map((module, index) => { + if (nodes.map((n) => n.type).includes(module)) { + return nodes.find((n) => n.type == module); + } + return { + id: `m_${index.toString()}`, + type: module, + position: { x: 200 + index * 200, y: 400 }, + }; + }) + .filter((e) => e); + + setNodes([initialNodes[0], ...newNodes]); + }, [props.modules]); + + const onConnect = useCallback( + (params) => setEdges((eds) => addEdge(params, eds)), + [setEdges], + ); + + return ( +
+ + + + {/* */} + +
+ ); +}; + +export default memo(SimulatorFlow); diff --git a/src/components/Simulator/index.js b/src/components/Simulator/index.js index d91d4f61..42114d5e 100644 --- a/src/components/Simulator/index.js +++ b/src/components/Simulator/index.js @@ -1,131 +1,111 @@ -import { useCallback, useEffect, memo } from "react"; -import { - ReactFlow, - Node, - Edge, - Background, - Controls, - MiniMap, - useNodesState, - useEdgesState, - addEdge, - Connection, -} from "@xyflow/react"; -import SenseBoxMCUS2 from "./nodes/mcu-s2"; -import "@xyflow/react/dist/style.css"; -import HDC1080 from "./nodes/hdc1080"; -import Display from "./nodes/display"; +import React, { useEffect, useState } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { startSimulator, stopSimulator } from "../../actions/simulatorActions"; +import IconButton from "@mui/material/IconButton"; +import { faPlay, faStop } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import SimulatorFlow from "./flow"; +import moment from "moment"; +import Box from "@mui/material/Box"; -const nodeTypes = { - board: SenseBoxMCUS2, - senseBox_hdc1080: HDC1080, - senseBox_display: Display, -}; - -const initialNodes = [ - { - id: "b_0", - type: "board", - position: { x: 400, y: 100 }, - }, - // { - // id: "2", - // type: "hdc1080", - // position: { x: 670, y: 400 }, - // data: { - // label: "Team Structure", - // description: "Who does what", - // icon: "team", - // }, - // }, - // { - // id: "3", - // type: "display", - // position: { x: 200, y: 400 }, - // data: { - // label: "Team Structure", - // description: "Who does what", - // icon: "team", - // }, - // }, -]; +export default function Simulator() { + const dispatch = useDispatch(); -const initialEdges = [ - // { id: "e1-2", source: "1", target: "2" }, - // { id: "e1-3", source: "1", target: "3" }, - // { id: "e1-4", source: "1", target: "4" }, - // { id: "e3-5", source: "3", target: "5" }, - // { id: "e2-5", source: "2", target: "5" }, - // { id: "e4-5", source: "4", target: "5" }, -]; + const simulationStartTimestamp = useSelector( + (state) => state.simulator.simulationStartTimestamp, + ); + const modules = useSelector((state) => state.simulator.modules); + const isSimulatorRunning = useSelector((state) => state.simulator.isRunning); + const [elapsedTime, setElapsedTime] = useState(0); -const SimulatorFlow = (props) => { - const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); - const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + console.log(modules); useEffect(() => { - // calculate new edges - const newEdges = []; - nodes.forEach((node) => { - if (node.type === "board") { - props.modules.forEach((module, index) => { - newEdges.push({ - id: `e${node.id}-${index}`, - source: node.id, - target: `m_${index}`, - }); - }); - } - }); + if (!isSimulatorRunning || !simulationStartTimestamp) { + setElapsedTime(0); + return; + } - setEdges([...initialEdges, ...newEdges]); - }, [nodes]); + const interval = setInterval(() => { + setElapsedTime(Date.now() - simulationStartTimestamp); + }, 10); // Update 10ms - useEffect(() => { - const newNodes = props.modules - .map((module, index) => { - if (nodes.map((n) => n.type).includes(module)) { - return nodes.find((n) => n.type == module); - } - return { - id: `m_${index.toString()}`, - type: module, - position: { x: 200 + index * 200, y: 400 }, - }; - }) - .filter((e) => e); + return () => clearInterval(interval); // Cleanup + }, [isSimulatorRunning, simulationStartTimestamp]); - setNodes([initialNodes[0], ...newNodes]); - }, [props.modules]); + // Handle start and stop actions + const handleStart = () => { + dispatch(startSimulator()); // Dispatch action to start simulation + }; - const onConnect = useCallback( - (params) => setEdges((eds) => addEdge(params, eds)), - [setEdges], - ); + const handleStop = () => { + dispatch(stopSimulator()); // Dispatch action to stop simulation + }; return (
- - - - {/* */} - + {isSimulatorRunning ? ( + + + + ) : ( + + + + )} +
+ + + +
+
+ + ); -}; +} + +const SimulationTimer = ({ elapsedTime }) => { + // If elapsedTime is less than 60, show seconds with decimals (e.g., 1.24s) + if (elapsedTime < 60000) { + const formattedTime = (elapsedTime / 1000).toFixed(1) + "s"; // Convert ms to seconds and format to 2 decimal places + return {formattedTime}; + } + + // If elapsedTime is greater than or equal to 60s, show in minutes and seconds (e.g., 3m 25s) + const duration = moment.duration(elapsedTime); -export default memo(SimulatorFlow); + const formattedTime = `${duration.minutes()}m ${duration.seconds()}s`; + + return {formattedTime}; +}; diff --git a/src/reducers/simulatorReducer.js b/src/reducers/simulatorReducer.js index 66567ab9..0c7ae8d2 100644 --- a/src/reducers/simulatorReducer.js +++ b/src/reducers/simulatorReducer.js @@ -7,6 +7,7 @@ const initialState = { modules: [], isRunning: false, interpreter: null, + simulationStartTimestamp: null, // Timestamp when the simulation starts abortController: null, // Added for abort control }; @@ -61,6 +62,7 @@ export default function simulatorReducer(state = initialState, action) { code: action.payload.simulator, modules: modules, interpreter: newInterpreter, + simulationStartTimestamp: null, abortController: null, // Reset abortController isRunning: false, }; @@ -86,6 +88,7 @@ export default function simulatorReducer(state = initialState, action) { return { ...state, isRunning: true, + simulationStartTimestamp: new Date(), abortController, // Save the controller for stopping }; } @@ -99,6 +102,7 @@ export default function simulatorReducer(state = initialState, action) { ...state, isRunning: false, interpreter: new Interpreter(state.code, initDom), // Reset interpreter + simulationStartTimestamp: null, abortController: null, // Clear the controller }; }