From 80e058b9e7c37bb16ac4d0a120d6d4e674d64ab9 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Fri, 18 Mar 2022 15:11:35 +0000 Subject: [PATCH 01/13] Quick simulator embedding. This simulator is very incomplete and experimental. It's UI placement will certainly need work for smaller screens! --- src/flags.ts | 6 ++- src/simulator/Simulator.tsx | 105 ++++++++++++++++++++++++++++++++++++ src/workbench/Workbench.tsx | 38 +++++++++---- 3 files changed, 139 insertions(+), 10 deletions(-) create mode 100644 src/simulator/Simulator.tsx diff --git a/src/flags.ts b/src/flags.ts index cb0773f28..e8c01b614 100644 --- a/src/flags.ts +++ b/src/flags.ts @@ -18,6 +18,10 @@ export type Flag = * Enables verbose debug logging to the console of drag events. */ | "dndDebug" + /** + * Simulator. Implementation is very incomplete. + */ + | "simulator" /** * Disables the pop-up welcome dialog. * The dialog is still available from the alpha release notice UI. @@ -28,7 +32,7 @@ export type Flag = */ | "noWelcome"; -const allFlags: Flag[] = ["dndDebug", "noWelcome"]; +const allFlags: Flag[] = ["dndDebug", "noWelcome", "simulator"]; type Flags = Record; diff --git a/src/simulator/Simulator.tsx b/src/simulator/Simulator.tsx new file mode 100644 index 000000000..117f4f279 --- /dev/null +++ b/src/simulator/Simulator.tsx @@ -0,0 +1,105 @@ +import { Box, HStack, IconButton, Stack } from "@chakra-ui/react"; +import { useCallback, useEffect, useRef } from "react"; +import { RiPlayFill, RiStopFill } from "react-icons/ri"; +import { MAIN_FILE } from "../fs/fs"; +import { useFileSystem } from "../fs/fs-hooks"; + +const useSimulator = (ref: React.RefObject) => { + const fs = useFileSystem(); + const readyCallbacks = useRef([] as Array<() => void>); + useEffect(() => { + if (ref.current) { + const messageListener = (e: MessageEvent) => { + const simulator = ref.current!.contentWindow; + if (e.source === simulator) { + switch (e.data.kind) { + case "ready": { + // TODO: sensors + while (readyCallbacks.current.length) { + readyCallbacks.current.pop()!(); + } + break; + } + case "serial_output": { + // TODO: serial + break; + } + } + } + }; + window.addEventListener("message", messageListener); + return () => { + window.removeEventListener("message", messageListener); + }; + } + }, [ref, readyCallbacks]); + + const play = useCallback(async () => { + // Temporary approach until we have simulator filesystem support. + const main = new TextDecoder().decode((await fs.read(MAIN_FILE)).data); + const simulator = ref.current!.contentWindow!; + simulator.postMessage( + { + kind: "serial_input", + // Ctrl-C to interrupt, Ctrl-D to reboot (straight to REPL as no program) + data: `\x03\x04`, + }, + "*" + ); + + // Wait for the ready message after the reboot. + await new Promise((resolve) => readyCallbacks.current.push(resolve)); + + simulator.postMessage( + { + kind: "serial_input", + // Ctrl-E to enter paste mode and Ctrl-D to finish + data: `\x05${main}\x04`.replace(/\n/g, "\r"), + }, + "*" + ); + }, [ref, fs]); + + const stop = useCallback(async () => { + const simulator = ref.current!.contentWindow!; + simulator.postMessage( + { + kind: "serial_input", + // Ctrl-C to interrupt. + data: `\x03\x04`, + }, + "*" + ); + }, [ref]); + + return { + play, + stop, + }; +}; + +const Simulator = () => { + const ref = useRef(null); + const { play, stop } = useSimulator(ref); + return ( + + + + } aria-label="Play" /> + } aria-label="Stop" /> + + + ); +}; + +export default Simulator; diff --git a/src/workbench/Workbench.tsx b/src/workbench/Workbench.tsx index 5c52718d2..38f715f93 100644 --- a/src/workbench/Workbench.tsx +++ b/src/workbench/Workbench.tsx @@ -16,10 +16,12 @@ import { SizedMode } from "../common/SplitView/SplitView"; import { ConnectionStatus } from "../device/device"; import { useConnectionStatus } from "../device/device-hooks"; import EditorArea from "../editor/EditorArea"; +import { flags } from "../flags"; import { MAIN_FILE } from "../fs/fs"; import { useProject } from "../project/project-hooks"; import ProjectActionBar from "../project/ProjectActionBar"; import SerialArea from "../serial/SerialArea"; +import Simulator from "../simulator/Simulator"; import SideBar from "./SideBar"; import { useSelection } from "./use-selection"; @@ -55,6 +57,18 @@ const Workbench = () => { const [serialStateWhenOpen, setSerialStateWhenOpen] = useState("compact"); const serialSizedMode = connected ? serialStateWhenOpen : "collapsed"; + const editor = ( + + {selection && fileVersion !== undefined && ( + + )} + + ); + return ( { mode={serialSizedMode} > - - {selection && fileVersion !== undefined && ( - - )} - + {flags.simulator ? ( + + {editor} + + + + + + ) : ( + editor + )} From e87cac0fdaccc5f15ad24e2702e5cd65b0e6559b Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Fri, 18 Mar 2022 17:10:01 +0000 Subject: [PATCH 02/13] Add the sensor. --- src/simulator/Simulator.tsx | 185 ++++++++++++++++++++++++++++++++---- 1 file changed, 165 insertions(+), 20 deletions(-) diff --git a/src/simulator/Simulator.tsx b/src/simulator/Simulator.tsx index 117f4f279..744448ac1 100644 --- a/src/simulator/Simulator.tsx +++ b/src/simulator/Simulator.tsx @@ -1,11 +1,34 @@ -import { Box, HStack, IconButton, Stack } from "@chakra-ui/react"; -import { useCallback, useEffect, useRef } from "react"; -import { RiPlayFill, RiStopFill } from "react-icons/ri"; +import { + AspectRatio, + Box, + BoxProps, + HStack, + Icon, + IconButton, + Slider, + SliderFilledTrack, + SliderMark, + SliderThumb, + SliderTrack, + Stack, + VStack, +} from "@chakra-ui/react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { RiPlayFill, RiStopFill, RiSunFill } from "react-icons/ri"; import { MAIN_FILE } from "../fs/fs"; import { useFileSystem } from "../fs/fs-hooks"; +interface Sensor { + type: "range"; + id: string; + min: number; + max: number; + value: number; +} + const useSimulator = (ref: React.RefObject) => { const fs = useFileSystem(); + const [sensors, setSensors] = useState>({}); const readyCallbacks = useRef([] as Array<() => void>); useEffect(() => { if (ref.current) { @@ -14,7 +37,13 @@ const useSimulator = (ref: React.RefObject) => { if (e.source === simulator) { switch (e.data.kind) { case "ready": { - // TODO: sensors + setSensors( + Object.fromEntries( + e.data.sensors.map((json: Sensor) => { + return [json.id, json]; + }) + ) + ); while (readyCallbacks.current.length) { readyCallbacks.current.pop()!(); } @@ -34,6 +63,25 @@ const useSimulator = (ref: React.RefObject) => { } }, [ref, readyCallbacks]); + const onSensorChange = useCallback( + (id: string, value: number) => { + setSensors((sensors) => ({ + ...sensors, + [id]: { ...sensors[id], value }, + })); + const simulator = ref.current!.contentWindow!; + simulator.postMessage( + { + kind: "sensor_set", + sensor: id, + value, + }, + "*" + ); + }, + [ref, setSensors] + ); + const play = useCallback(async () => { // Temporary approach until we have simulator filesystem support. const main = new TextDecoder().decode((await fs.read(MAIN_FILE)).data); @@ -75,31 +123,128 @@ const useSimulator = (ref: React.RefObject) => { return { play, stop, + sensors: Object.values(sensors), + onSensorChange, }; }; const Simulator = () => { const ref = useRef(null); - const { play, stop } = useSimulator(ref); + const { play, stop, sensors, onSensorChange } = useSimulator(ref); return ( - - + + + + + {/* Margin hack until we remove space from iframe */} + + } + aria-label="Run" + /> + } + aria-label="Stop" + /> + + + - - } aria-label="Play" /> - } aria-label="Stop" /> - + + ); +}; + +interface SensorsProps extends BoxProps { + value: Sensor[]; + onSensorChange: (id: string, value: number) => void; +} + +const Sensors = ({ value, onSensorChange, ...props }: SensorsProps) => { + return ( + + {value.map((sensor) => { + switch (sensor.type) { + case "range": + return ( + + ); + default: + return null; + } + })} ); }; +interface RangeSensorProps { + value: Sensor; + onSensorChange: (id: string, value: number) => void; +} + +const RangeSensor = ({ + value: { id, min, max, value }, + onSensorChange, +}: RangeSensorProps) => { + const handleChange = useCallback( + (value: number) => { + onSensorChange(id, value); + }, + [onSensorChange, id] + ); + return ( + + + + + + + + + {min} + + + {max} + + + {value} + + + + ); +}; + export default Simulator; From b40e50551cb0286cdf53fe484fe353100a7c9907 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Fri, 18 Mar 2022 18:40:47 +0000 Subject: [PATCH 03/13] Safari fix. --- src/simulator/Simulator.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/simulator/Simulator.tsx b/src/simulator/Simulator.tsx index 744448ac1..95bb377bb 100644 --- a/src/simulator/Simulator.tsx +++ b/src/simulator/Simulator.tsx @@ -237,7 +237,6 @@ const RangeSensor = ({ textAlign="center" mt="-8" ml="-1.5ch" - w="3ch" fontSize="sm" > {value} From 51a7f32ea45c3fb0f6fdccf0d94ae3332bf0f3df Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Sat, 19 Mar 2022 20:48:48 +0000 Subject: [PATCH 04/13] More sensor support. --- src/simulator/Simulator.tsx | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/simulator/Simulator.tsx b/src/simulator/Simulator.tsx index 95bb377bb..9a4e4aca2 100644 --- a/src/simulator/Simulator.tsx +++ b/src/simulator/Simulator.tsx @@ -14,7 +14,13 @@ import { VStack, } from "@chakra-ui/react"; import { useCallback, useEffect, useRef, useState } from "react"; -import { RiPlayFill, RiStopFill, RiSunFill } from "react-icons/ri"; +import { IconType } from "react-icons"; +import { + RiPlayFill, + RiStopFill, + RiSunFill, + RiTempHotFill, +} from "react-icons/ri"; import { MAIN_FILE } from "../fs/fs"; import { useFileSystem } from "../fs/fs-hooks"; @@ -24,6 +30,7 @@ interface Sensor { min: number; max: number; value: number; + unit?: string; } const useSimulator = (ref: React.RefObject) => { @@ -177,7 +184,7 @@ interface SensorsProps extends BoxProps { const Sensors = ({ value, onSensorChange, ...props }: SensorsProps) => { return ( - + {value.map((sensor) => { switch (sensor.type) { case "range": @@ -201,8 +208,13 @@ interface RangeSensorProps { onSensorChange: (id: string, value: number) => void; } +const icons: Record = { + temperature: RiTempHotFill, + lightLevel: RiSunFill, +}; + const RangeSensor = ({ - value: { id, min, max, value }, + value: { id, min, max, value, unit }, onSensorChange, }: RangeSensorProps) => { const handleChange = useCallback( @@ -211,9 +223,10 @@ const RangeSensor = ({ }, [onSensorChange, id] ); + const valueText = unit ? `${value} ${unit}` : value.toString(); return ( - - + + - {value} + {valueText} From 97088c72525b4212aaa3488b820e129a3f04e25a Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Sat, 19 Mar 2022 22:09:53 +0000 Subject: [PATCH 05/13] Placeholder icons. --- src/simulator/Simulator.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/simulator/Simulator.tsx b/src/simulator/Simulator.tsx index 9a4e4aca2..d467acb94 100644 --- a/src/simulator/Simulator.tsx +++ b/src/simulator/Simulator.tsx @@ -17,6 +17,8 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { IconType } from "react-icons"; import { RiPlayFill, + RiQuestionFill, + RiSensorFill, RiStopFill, RiSunFill, RiTempHotFill, @@ -226,7 +228,12 @@ const RangeSensor = ({ const valueText = unit ? `${value} ${unit}` : value.toString(); return ( - + {valueText} From 9eed091231e2b273eb6f5f691b667ce1ff565c3e Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Sat, 19 Mar 2022 22:21:31 +0000 Subject: [PATCH 06/13] Lint. --- src/simulator/Simulator.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/simulator/Simulator.tsx b/src/simulator/Simulator.tsx index d467acb94..e29fa687a 100644 --- a/src/simulator/Simulator.tsx +++ b/src/simulator/Simulator.tsx @@ -18,7 +18,6 @@ import { IconType } from "react-icons"; import { RiPlayFill, RiQuestionFill, - RiSensorFill, RiStopFill, RiSunFill, RiTempHotFill, From 044dd3ca9aac1e57b6d7bbf2c67254b99a766913 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Tue, 21 Jun 2022 16:29:11 +0100 Subject: [PATCH 07/13] Use new flash message on simulator branch. --- src/simulator/Simulator.tsx | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/simulator/Simulator.tsx b/src/simulator/Simulator.tsx index e29fa687a..25699fd9f 100644 --- a/src/simulator/Simulator.tsx +++ b/src/simulator/Simulator.tsx @@ -22,7 +22,6 @@ import { RiSunFill, RiTempHotFill, } from "react-icons/ri"; -import { MAIN_FILE } from "../fs/fs"; import { useFileSystem } from "../fs/fs-hooks"; interface Sensor { @@ -91,26 +90,19 @@ const useSimulator = (ref: React.RefObject) => { ); const play = useCallback(async () => { - // Temporary approach until we have simulator filesystem support. - const main = new TextDecoder().decode((await fs.read(MAIN_FILE)).data); - const simulator = ref.current!.contentWindow!; - simulator.postMessage( - { - kind: "serial_input", - // Ctrl-C to interrupt, Ctrl-D to reboot (straight to REPL as no program) - data: `\x03\x04`, - }, - "*" + const filesystem: Record = Object.fromEntries( + await Promise.all( + fs.project.files.map(async (f) => [ + f.name, + (await fs.read(f.name)).data, + ]) + ) ); - - // Wait for the ready message after the reboot. - await new Promise((resolve) => readyCallbacks.current.push(resolve)); - + const simulator = ref.current!.contentWindow!; simulator.postMessage( { - kind: "serial_input", - // Ctrl-E to enter paste mode and Ctrl-D to finish - data: `\x05${main}\x04`.replace(/\n/g, "\r"), + kind: "flash", + filesystem, }, "*" ); @@ -122,7 +114,8 @@ const useSimulator = (ref: React.RefObject) => { { kind: "serial_input", // Ctrl-C to interrupt. - data: `\x03\x04`, + // A specific message would be useful as probably best to clear display etc. here. + data: `\x03`, }, "*" ); From d2e107bb036d1e97a50755176ed7285cbe7f68a5 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Thu, 21 Jul 2022 13:07:57 +0100 Subject: [PATCH 08/13] Use microbit-owned simulator URL. --- src/simulator/Simulator.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/simulator/Simulator.tsx b/src/simulator/Simulator.tsx index 25699fd9f..8052e7c84 100644 --- a/src/simulator/Simulator.tsx +++ b/src/simulator/Simulator.tsx @@ -140,7 +140,7 @@ const Simulator = () => { ref={ref} as="iframe" // Very much a temporary / unsupported deployment! - src="https://distracted-dubinsky-fd8a42.netlify.app/simulator.html" + src="https://stage-python-simulator.microbit.org/simulator.html" title="Simulator" frameBorder="no" scrolling="no" From fe90326bbad4c52cbc22eaff60a4194e5fffd95a Mon Sep 17 00:00:00 2001 From: Matt Hillsdon <44397098+microbit-matt-hillsdon@users.noreply.github.com> Date: Thu, 21 Jul 2022 13:13:15 +0100 Subject: [PATCH 09/13] Tidying --- src/documentation/common/DocumentationContent.tsx | 10 ---------- src/simulator/Simulator.tsx | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/documentation/common/DocumentationContent.tsx b/src/documentation/common/DocumentationContent.tsx index 84fe44134..c490938a5 100644 --- a/src/documentation/common/DocumentationContent.tsx +++ b/src/documentation/common/DocumentationContent.tsx @@ -175,16 +175,6 @@ const serializers = { // We use a fragment so we can use spacing from the context into which we render. container: ({ children }: HasChildren) => <>{children}, types: { - block: (props: { node: { style: string }; children: any }) => { - const style = props.node.style; - if (/^h\d/.test(style)) { - return ( - - {BlockContent.defaultSerializers.types.block(props)} - - ); - } - }, collapse: ({ node, }: SerializerNodeProps<{ diff --git a/src/simulator/Simulator.tsx b/src/simulator/Simulator.tsx index 8052e7c84..44536dc81 100644 --- a/src/simulator/Simulator.tsx +++ b/src/simulator/Simulator.tsx @@ -139,7 +139,7 @@ const Simulator = () => { Date: Thu, 21 Jul 2022 13:22:14 +0100 Subject: [PATCH 10/13] More unintended change --- src/documentation/common/DocumentationContent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/documentation/common/DocumentationContent.tsx b/src/documentation/common/DocumentationContent.tsx index c490938a5..fbf0415ab 100644 --- a/src/documentation/common/DocumentationContent.tsx +++ b/src/documentation/common/DocumentationContent.tsx @@ -5,7 +5,7 @@ */ import Icon from "@chakra-ui/icon"; import { Image } from "@chakra-ui/image"; -import { Box, Heading, Link, Stack } from "@chakra-ui/layout"; +import { Box, Link, Stack } from "@chakra-ui/layout"; import { Collapse } from "@chakra-ui/react"; import BlockContent from "@sanity/block-content-to-react"; import React, { ReactNode, useContext, useMemo } from "react"; From 29734a4353a4c23d19a0b2c72c379564a05c9cab Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Tue, 26 Jul 2022 13:06:56 +0100 Subject: [PATCH 11/13] Split the simulator up. --- src/simulator/RangeSensor.tsx | 70 ++++++++++ src/simulator/Sensors.tsx | 31 +++++ src/simulator/Simulator.tsx | 222 +----------------------------- src/simulator/model.ts | 16 +++ src/simulator/simulator-hooks.tsx | 98 +++++++++++++ 5 files changed, 220 insertions(+), 217 deletions(-) create mode 100644 src/simulator/RangeSensor.tsx create mode 100644 src/simulator/Sensors.tsx create mode 100644 src/simulator/model.ts create mode 100644 src/simulator/simulator-hooks.tsx diff --git a/src/simulator/RangeSensor.tsx b/src/simulator/RangeSensor.tsx new file mode 100644 index 000000000..82cde2b9b --- /dev/null +++ b/src/simulator/RangeSensor.tsx @@ -0,0 +1,70 @@ +import { Icon } from "@chakra-ui/icons"; +import { + HStack, + Slider, + SliderFilledTrack, + SliderMark, + SliderThumb, + SliderTrack, +} from "@chakra-ui/react"; +import { useCallback } from "react"; +import { RiQuestionFill } from "react-icons/ri"; +import { Sensor, sensorIcons } from "./model"; + +interface RangeSensorProps { + value: Sensor; + onSensorChange: (id: string, value: number) => void; +} + +const RangeSensor = ({ + value: { id, min, max, value, unit }, + onSensorChange, +}: RangeSensorProps) => { + const handleChange = useCallback( + (value: number) => { + onSensorChange(id, value); + }, + [onSensorChange, id] + ); + const valueText = unit ? `${value} ${unit}` : value.toString(); + return ( + + + + + + + + + {min} + + + {max} + + + {valueText} + + + + ); +}; + +export default RangeSensor; diff --git a/src/simulator/Sensors.tsx b/src/simulator/Sensors.tsx new file mode 100644 index 000000000..a7e521406 --- /dev/null +++ b/src/simulator/Sensors.tsx @@ -0,0 +1,31 @@ +import { BoxProps, Stack } from "@chakra-ui/react"; +import { Sensor } from "./model"; +import RangeSensor from "./RangeSensor"; + +interface SensorsProps extends BoxProps { + value: Sensor[]; + onSensorChange: (id: string, value: number) => void; +} + +const Sensors = ({ value, onSensorChange, ...props }: SensorsProps) => { + return ( + + {value.map((sensor) => { + switch (sensor.type) { + case "range": + return ( + + ); + default: + return null; + } + })} + + ); +}; + +export default Sensors; diff --git a/src/simulator/Simulator.tsx b/src/simulator/Simulator.tsx index 44536dc81..0ef977380 100644 --- a/src/simulator/Simulator.tsx +++ b/src/simulator/Simulator.tsx @@ -1,133 +1,8 @@ -import { - AspectRatio, - Box, - BoxProps, - HStack, - Icon, - IconButton, - Slider, - SliderFilledTrack, - SliderMark, - SliderThumb, - SliderTrack, - Stack, - VStack, -} from "@chakra-ui/react"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { IconType } from "react-icons"; -import { - RiPlayFill, - RiQuestionFill, - RiStopFill, - RiSunFill, - RiTempHotFill, -} from "react-icons/ri"; -import { useFileSystem } from "../fs/fs-hooks"; - -interface Sensor { - type: "range"; - id: string; - min: number; - max: number; - value: number; - unit?: string; -} - -const useSimulator = (ref: React.RefObject) => { - const fs = useFileSystem(); - const [sensors, setSensors] = useState>({}); - const readyCallbacks = useRef([] as Array<() => void>); - useEffect(() => { - if (ref.current) { - const messageListener = (e: MessageEvent) => { - const simulator = ref.current!.contentWindow; - if (e.source === simulator) { - switch (e.data.kind) { - case "ready": { - setSensors( - Object.fromEntries( - e.data.sensors.map((json: Sensor) => { - return [json.id, json]; - }) - ) - ); - while (readyCallbacks.current.length) { - readyCallbacks.current.pop()!(); - } - break; - } - case "serial_output": { - // TODO: serial - break; - } - } - } - }; - window.addEventListener("message", messageListener); - return () => { - window.removeEventListener("message", messageListener); - }; - } - }, [ref, readyCallbacks]); - - const onSensorChange = useCallback( - (id: string, value: number) => { - setSensors((sensors) => ({ - ...sensors, - [id]: { ...sensors[id], value }, - })); - const simulator = ref.current!.contentWindow!; - simulator.postMessage( - { - kind: "sensor_set", - sensor: id, - value, - }, - "*" - ); - }, - [ref, setSensors] - ); - - const play = useCallback(async () => { - const filesystem: Record = Object.fromEntries( - await Promise.all( - fs.project.files.map(async (f) => [ - f.name, - (await fs.read(f.name)).data, - ]) - ) - ); - const simulator = ref.current!.contentWindow!; - simulator.postMessage( - { - kind: "flash", - filesystem, - }, - "*" - ); - }, [ref, fs]); - - const stop = useCallback(async () => { - const simulator = ref.current!.contentWindow!; - simulator.postMessage( - { - kind: "serial_input", - // Ctrl-C to interrupt. - // A specific message would be useful as probably best to clear display etc. here. - data: `\x03`, - }, - "*" - ); - }, [ref]); - - return { - play, - stop, - sensors: Object.values(sensors), - onSensorChange, - }; -}; +import { AspectRatio, Box, HStack, IconButton, VStack } from "@chakra-ui/react"; +import { useRef } from "react"; +import { RiPlayFill, RiStopFill } from "react-icons/ri"; +import Sensors from "./Sensors"; +import { useSimulator } from "./simulator-hooks"; const Simulator = () => { const ref = useRef(null); @@ -171,91 +46,4 @@ const Simulator = () => { ); }; -interface SensorsProps extends BoxProps { - value: Sensor[]; - onSensorChange: (id: string, value: number) => void; -} - -const Sensors = ({ value, onSensorChange, ...props }: SensorsProps) => { - return ( - - {value.map((sensor) => { - switch (sensor.type) { - case "range": - return ( - - ); - default: - return null; - } - })} - - ); -}; - -interface RangeSensorProps { - value: Sensor; - onSensorChange: (id: string, value: number) => void; -} - -const icons: Record = { - temperature: RiTempHotFill, - lightLevel: RiSunFill, -}; - -const RangeSensor = ({ - value: { id, min, max, value, unit }, - onSensorChange, -}: RangeSensorProps) => { - const handleChange = useCallback( - (value: number) => { - onSensorChange(id, value); - }, - [onSensorChange, id] - ); - const valueText = unit ? `${value} ${unit}` : value.toString(); - return ( - - - - - - - - - {min} - - - {max} - - - {valueText} - - - - ); -}; - export default Simulator; diff --git a/src/simulator/model.ts b/src/simulator/model.ts new file mode 100644 index 000000000..37a20a211 --- /dev/null +++ b/src/simulator/model.ts @@ -0,0 +1,16 @@ +import { IconType } from "react-icons"; +import { RiSunFill, RiTempHotFill } from "react-icons/ri"; + +export const sensorIcons: Record = { + temperature: RiTempHotFill, + lightLevel: RiSunFill, +}; + +export interface Sensor { + type: "range"; + id: string; + min: number; + max: number; + value: number; + unit?: string; +} diff --git a/src/simulator/simulator-hooks.tsx b/src/simulator/simulator-hooks.tsx new file mode 100644 index 000000000..d1442d7c1 --- /dev/null +++ b/src/simulator/simulator-hooks.tsx @@ -0,0 +1,98 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { useFileSystem } from "../fs/fs-hooks"; + +export const useSimulator = (ref: React.RefObject) => { + const fs = useFileSystem(); + const [sensors, setSensors] = useState>({}); + const readyCallbacks = useRef([] as Array<() => void>); + useEffect(() => { + if (ref.current) { + const messageListener = (e: MessageEvent) => { + const simulator = ref.current!.contentWindow; + if (e.source === simulator) { + switch (e.data.kind) { + case "ready": { + setSensors( + Object.fromEntries( + e.data.sensors.map((json: Sensor) => { + return [json.id, json]; + }) + ) + ); + while (readyCallbacks.current.length) { + readyCallbacks.current.pop()!(); + } + break; + } + case "serial_output": { + // TODO: serial + break; + } + } + } + }; + window.addEventListener("message", messageListener); + return () => { + window.removeEventListener("message", messageListener); + }; + } + }, [ref, readyCallbacks]); + + const onSensorChange = useCallback( + (id: string, value: number) => { + setSensors((sensors) => ({ + ...sensors, + [id]: { ...sensors[id], value }, + })); + const simulator = ref.current!.contentWindow!; + simulator.postMessage( + { + kind: "sensor_set", + sensor: id, + value, + }, + "*" + ); + }, + [ref, setSensors] + ); + + const play = useCallback(async () => { + const filesystem: Record = Object.fromEntries( + await Promise.all( + fs.project.files.map(async (f) => [ + f.name, + (await fs.read(f.name)).data, + ]) + ) + ); + const simulator = ref.current!.contentWindow!; + simulator.postMessage( + { + kind: "flash", + filesystem, + }, + "*" + ); + }, [ref, fs]); + + const stop = useCallback(async () => { + const simulator = ref.current!.contentWindow!; + simulator.postMessage( + { + kind: "serial_input", + // Ctrl-C to interrupt. + // A specific message would be useful as probably best to clear display etc. here. + data: `\x03`, + }, + "*" + ); + }, [ref]); + + return { + play, + stop, + sensors: Object.values(sensors), + onSensorChange, + }; +}; From 6d1a4cbbff67671bf51157ea000e19cf7ebef8a2 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Tue, 26 Jul 2022 13:09:46 +0100 Subject: [PATCH 12/13] Import fixup. --- src/simulator/simulator-hooks.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/simulator/simulator-hooks.tsx b/src/simulator/simulator-hooks.tsx index d1442d7c1..047986a19 100644 --- a/src/simulator/simulator-hooks.tsx +++ b/src/simulator/simulator-hooks.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useFileSystem } from "../fs/fs-hooks"; +import { Sensor } from "./model"; export const useSimulator = (ref: React.RefObject) => { const fs = useFileSystem(); From 8724b05b39fa9b36173ef47cccbf42a5b27d9d36 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Tue, 26 Jul 2022 13:59:58 +0100 Subject: [PATCH 13/13] Fix ratio remove hack. --- src/simulator/Simulator.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/simulator/Simulator.tsx b/src/simulator/Simulator.tsx index 0ef977380..c7ce517db 100644 --- a/src/simulator/Simulator.tsx +++ b/src/simulator/Simulator.tsx @@ -10,7 +10,7 @@ const Simulator = () => { return ( - + { scrolling="no" /> - {/* Margin hack until we remove space from iframe */} - +