-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Simulator embedding (very incomplete, behind flag) (#816)
The simulator is deployed separately from https://github.com/microbit-foundation/micropython-microbit-v2-simulator and included via an iframe. It is not ready for use and is being merged behind a flag to enable iterative development of the UI and simulator features.
- Loading branch information
1 parent
8bd6039
commit 90b7f07
Showing
7 changed files
with
299 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<HStack pt={5}> | ||
<Icon | ||
as={sensorIcons[id] || RiQuestionFill} | ||
aria-label={id} | ||
color="blimpTeal.400" | ||
boxSize="6" | ||
/> | ||
<Slider | ||
aria-label={id} | ||
value={value} | ||
min={min} | ||
max={max} | ||
onChange={handleChange} | ||
my={5} | ||
> | ||
<SliderTrack> | ||
<SliderFilledTrack bgColor="blimpTeal.600" /> | ||
</SliderTrack> | ||
<SliderThumb /> | ||
<SliderMark value={min} mt="1" fontSize="xs"> | ||
{min} | ||
</SliderMark> | ||
<SliderMark value={max} mt="1" ml="-3ch" fontSize="xs"> | ||
{max} | ||
</SliderMark> | ||
<SliderMark | ||
value={value} | ||
textAlign="center" | ||
mt="-8" | ||
ml={-valueText.length / 2 + "ch"} | ||
fontSize="xs" | ||
> | ||
{valueText} | ||
</SliderMark> | ||
</Slider> | ||
</HStack> | ||
); | ||
}; | ||
|
||
export default RangeSensor; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<Stack {...props} height="100%" width="100%" p={5} spacing={3}> | ||
{value.map((sensor) => { | ||
switch (sensor.type) { | ||
case "range": | ||
return ( | ||
<RangeSensor | ||
key={sensor.id} | ||
value={sensor} | ||
onSensorChange={onSensorChange} | ||
/> | ||
); | ||
default: | ||
return null; | ||
} | ||
})} | ||
</Stack> | ||
); | ||
}; | ||
|
||
export default Sensors; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
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<HTMLIFrameElement>(null); | ||
const { play, stop, sensors, onSensorChange } = useSimulator(ref); | ||
return ( | ||
<VStack spacing={5}> | ||
<Box width="100%"> | ||
<AspectRatio ratio={191.27 / 155.77} width="100%"> | ||
<Box | ||
ref={ref} | ||
as="iframe" | ||
// This needs changing before we remove the flag. | ||
src="https://stage-python-simulator.microbit.org/simulator.html" | ||
title="Simulator" | ||
frameBorder="no" | ||
scrolling="no" | ||
/> | ||
</AspectRatio> | ||
<HStack justifyContent="center"> | ||
<IconButton | ||
variant="solid" | ||
onClick={play} | ||
icon={<RiPlayFill />} | ||
aria-label="Run" | ||
/> | ||
<IconButton | ||
variant="outline" | ||
onClick={stop} | ||
icon={<RiStopFill />} | ||
aria-label="Stop" | ||
/> | ||
</HStack> | ||
</Box> | ||
<Sensors | ||
value={sensors} | ||
flex="1 1 auto" | ||
onSensorChange={onSensorChange} | ||
/> | ||
</VStack> | ||
); | ||
}; | ||
|
||
export default Simulator; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { IconType } from "react-icons"; | ||
import { RiSunFill, RiTempHotFill } from "react-icons/ri"; | ||
|
||
export const sensorIcons: Record<string, IconType> = { | ||
temperature: RiTempHotFill, | ||
lightLevel: RiSunFill, | ||
}; | ||
|
||
export interface Sensor { | ||
type: "range"; | ||
id: string; | ||
min: number; | ||
max: number; | ||
value: number; | ||
unit?: string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import { useCallback, useEffect, useRef, useState } from "react"; | ||
import { useFileSystem } from "../fs/fs-hooks"; | ||
import { Sensor } from "./model"; | ||
|
||
export const useSimulator = (ref: React.RefObject<HTMLIFrameElement>) => { | ||
const fs = useFileSystem(); | ||
const [sensors, setSensors] = useState<Record<string, Sensor>>({}); | ||
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<string, Uint8Array> = 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, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters