diff --git a/PSIM_SPEC.md b/PSIM_SPEC.md new file mode 100644 index 00000000000..04eeee0af9d --- /dev/null +++ b/PSIM_SPEC.md @@ -0,0 +1,207 @@ +# PSIM: The Physical Simulator for MakeCode + +## Context + +Micro:bits are meant to move around, inside and outside the classroom. They are small, battery-powered, and can be attached to people, things, etc. They can detect motion, as well as sound, light and temperature levels.  They have a radio so they can be connected wirelessly into adhoc networks (with a limited ability to detect proximity to other microbits). The success/failure of radio transmission depends on the location of micro:bits. More complex projects may use multiple microbits moving in an environment, sensing, reacting, and communicating with one another. Debugging such little distributed systems can be difficult.  + +## High-level goal + +We want to create a new simulator of multiple microbits in a physical environment (PSIM), building off of the PXT framework (which already features a microbit simulator that makes most of the functions of the microbit available to the end user on the left side of the app).  This should help users gain a better understanding of the behavior of their system. + +## Assumptions + +We assume that each micro:bit will run the same user program, so micro:bits can be put into different modes by the user using micro:bit buttons (or radio messages); in the future, one should be able to load different programs into PSIM. + +## Launching and basic view + +PSIM is launched by a new button in the micro:bit simulator toolbar (simtoolbar.tsx) and takes over the editor (like serialEditor: serial.Editor) +The physical simulator is populated with a single micro:bit to start, which is running the user's program.  It represents the micro:bits in a +plane (2D only). Each micro:bit is shown using a micro:bit sprite (shows state of the LED matrix, should reuse the mbit SVG). + +## Micro:bit metadata + +Each micro:bit sprite has a set of properties, including: +- Friendly name (for referring to it and sending it events) +- X and Y coordinates in the plane +- Associated PXT simulator (iframe) + +## PSIM features + +The PSIM toolbar allows you to + +- back out of PSIM, as in the serialeditor +- create a new micro:bit sprite and associapted simulator, running a new (named) instance of the user's program; + per the PXT framework, each new microbit simulator is hosted in a new iframe in the side panel + +Actions on the canvas: + +- When you select a micro:bit sprite in the PSIM canvas, the sidepanel focus on the sprite's corresponding iframe + +- You can move a micro:bit sprite around in the PSIM to change its (x,y) location + +Modelling wireless radio messages + +The PSIM models the radio transmission of packets from micro:bits (which are messages send to and received from the iframe) +in physical space and determines which micro:bits can hear others (based on signal strength). As a bonus, it show animations +of microbits sending/receiving radio packets. Also need to add info from physical space into the radio packet (especially signalstrength).  + +## Relevant info on radio + +### Make the ``radio`` signal of the @boardname@ stronger or weaker. + +```sig +radio.setTransmitPower(7); +``` + +The signal can be as weak as `0` and as strong as `7`. Default is ``6``. + +The scientific name for the strength of the ``radio`` signal is +**dBm**, or **decibel-milliwatts**. A signal strength of `0` +can be measured as -30 dBm, and a strength of `7` can be +measured as +4 dBm. + +If your @boardname@ is sending with a strength of `7`, and you are in +an open area without many other computers around, the @boardname@ signal +can reach as far as 70 meters (about 230 feet). + + +### Get the signal strength of the last received packet. + +``` +let ss = radio.receivedPacket(RadioPacketProperty.SignalStrength) +``` + +### related work + +- https://github.com/behbad/Bluetooth-Low-Energy-5-System-Level-Simulator-BLE5 + +## Test drivers for PSIM + +We want to be able to code against the named microbits to generate the user-level events (using the CODAL events) as well as to +intercept outputs/events generated by the microbit (such as radio messages) and decide what to do with them.  + +We will start with using plain TypeScript (not Static TypeScript) to give us more access to simulator state and features. +In the future, maybe provide bindings so test automation can be written as part of PXT project in Static TypeScript. + +We also want to program how the micro:bits move in space. + +## Work plan + +- PSIM should have mapping from friendly names meta data for each simulated micro:bit, which includes the id of the + iframe that contains the micro:bit simulator +- display each microbit using image, scaled down, as part of microbit sprite + X tag each microbit sprite with its friendly name + X allow microbit sprites to be selected and moved around with mouse + - on selection of a microbit sprite in the PSIM, bring focus to the corresponding iframe (micro:bit simulator) +- deal with updates to relevent mbit state + - radio transmit strength (maps to circle around microbit, modelling its transmit range) + - LED matrix update + - color of the microbit + - microbit makes a soiund (animation) + - radio send message (anumation) +X the PSIM should intercept radio messages sent from the micro:bit simulator iframes and create an animation + in the PSIM around the corresponding microbit sprite + X this requires quite a bit of new plumbing: + X intercept SimulatorBroadcastMessages posted to simdriver by one of the simFrames, identified by srcFrameIndex + X instead of passing them down to simFrames, need to pass them up and out to the PSIM, which will then + determine which ones to pass back (and who to address them to) + X we already have logic for passing message up to parentWindow, but we also pass the same message down +- We want the PSIM to determine which microbit sprites can "hear" a radio message sent by a microbit, + depending on the radio transmit strength of each microbit and the Euclidean distance between microbit sprites + in the PSIM; it then will determine which microbit sprites will receive the message and send it to the + corresponding simulators +- Create a test framework that allows us to program against the microbits in the PSIM. This includes APIs for + - creating a new microbit and naming it (same API as for PSIM toolbar button) + - send a CODAL event to the named microbit (the EventBus for the particular sim) + X intercept all microbit outputs (not clear how we do this yet; may need to instrument + simulator thunks and send messages out of iframe; could probably use compiler support + to do this automagically for everything that has a shim annotation) +- Allow multiple MakeCode programs to be loaded into the PSIM + + +## Helpful plumbing + +- for testing, we need to be able to intercept calls from user code to libraries (ala SLIC?) +X instrumentation can easily be done in JavaScript for the simulator + +### Example + +We want to capture the number sent to the display via `basic.showNumber(i)`, where +the function is declared as follows: +``` +namespace basic { + ... + export function showNumber(value: number, interval?: number) { ... } + ... +} +``` + +This is supported by the simulator as follows: +``` +namespace pxsim.basic { + export function showNumber(x: number, interval: number) { ... } +} +``` +So, there are two ways we could proceed +- instrument the user code (caller instrumentation) by adding support in the compiler +- instrument the pxsim code (callee instrumentation) by reflection + +### output functions + +#### The basic ones dealing with the LED screen + +``` +basic.showNumber(0) +basic.showLeds(` + # . . . # + . # . # . + . . # . . + . . . . . + . . . . . + `) +basic.showIcon(IconNames.Heart) +basic.showString("Hello!") +basic.showArrow(ArrowNames.North) +basic.clearScreen() +``` + +#### plotting to the LED screen + +``` +led.plot(0, 0) +led.toggle(0, 0) +led.unplot(0, 0) +led.plotBarGraph(0, 0) +led.plotBrightness(0, 0, 255) +led.setBrightness(255) +led.enable(false) +led.stopAnimation() +``` + +#### speaker + +``` +music.stopAllSounds() +music.setBuiltInSpeakerEnabled(false) +music.setVolume(127) +music.ringTone(Note.C) +``` + +#### radio + +``` +radio.sendNumber(0) +radio.sendValue("name", 0) +radio.sendString("") +radio.setGroup(1) +radio.setTransmitPower(7) +radio.setTransmitSerialNumber(true) +radio.setFrequencyBand(0). /// ? +``` + +### pins + +``` +pins.digitalWritePin(DigitalPin.P0, 0) +pins.analogWritePin(AnalogPin.P0, 1023) +``` diff --git a/docs/static/experiments/physicalSimulator.png b/docs/static/experiments/physicalSimulator.png new file mode 100644 index 00000000000..98533f7cdb2 Binary files /dev/null and b/docs/static/experiments/physicalSimulator.png differ diff --git a/libs/core/_locales/core-jsdoc-strings.json b/libs/core/_locales/core-jsdoc-strings.json index 9b296a2d54b..4d7055343ed 100644 --- a/libs/core/_locales/core-jsdoc-strings.json +++ b/libs/core/_locales/core-jsdoc-strings.json @@ -151,6 +151,9 @@ "Math.ceil": "Returns the smallest number greater than or equal to its numeric argument.", "Math.ceil|param|x": "A numeric expression.", "Math.constrain": "Constrains a number to be within a range", + "Math.convert": "Converts a value from one unit to another. For example, degrees to radians, fahrenheit to celsius, etc.", + "Math.convert|param|type": "The type of conversion to perform.", + "Math.convert|param|value": "The value to convert.", "Math.cos": "Returns the cosine of a number.", "Math.cos|param|x": "An angle in radians", "Math.exp": "Returns returns ``e^x``.", @@ -275,6 +278,25 @@ "basic.showString": "Display text on the display, one character at a time. If the string fits on the screen (i.e. is one letter), does not scroll.", "basic.showString|param|interval": "how fast to shift characters; eg: 150, 100, 200, -100", "basic.showString|param|text": "the text to scroll on the screen, eg: \"Hello!\"", + "colorHelpers.cmyk": "Converts a CMYK color into a single color number.\n*\n\n\n\n\n@returns The combined color as a single number", + "colorHelpers.cmyk|param|black": "The black component of the color, between 0 and 100", + "colorHelpers.cmyk|param|cyan": "The cyan component of the color, between 0 and 100", + "colorHelpers.cmyk|param|magenta": "The magenta component of the color, between 0 and 100", + "colorHelpers.cmyk|param|yellow": "The yellow component of the color, between 0 and 100", + "colorHelpers.hex": "Converts a hexadecimal color string into a single color number. The hexadecimal string can be in the short\n3-digit form (\"#f0a\") or the full 6-digit form (\"#ff00aa\").\n*\n\n@returns The combined color as a single number", + "colorHelpers.hex|param|hex": "A hexadecimal color string, optionally starting with \"#\" and either in 3-digit or 6-digit format", + "colorHelpers.hsl": "Converts a hue, saturation, and lightness (HSL) color into a single color number.\n*\n\n\n\n@returns The combined color as a single number", + "colorHelpers.hsl|param|hue": "The hue component of the color, between 0 and 360", + "colorHelpers.hsl|param|lightness": "The lightness component of the color, between 0 and 100", + "colorHelpers.hsl|param|saturation": "The saturation component of the color, between 0 and 100", + "colorHelpers.hsv": "Converts a hue, saturation, and value (HSV) color into a single color number.\n*\n\n\n\n@returns The combined color as a single number", + "colorHelpers.hsv|param|hue": "The hue component of the color, between 0 and 360", + "colorHelpers.hsv|param|saturation": "The saturation component of the color, between 0 and 100", + "colorHelpers.hsv|param|value": "The value component of the color, between 0 and 100", + "colorHelpers.rgb": "Converts a red, green, and blue color value into a single color number.\n*\n\n\n\n@returns The combined color as a single number", + "colorHelpers.rgb|param|blue": "The blue component of the color, between 0 and 255", + "colorHelpers.rgb|param|green": "The green component of the color, between 0 and 255", + "colorHelpers.rgb|param|red": "The red component of the color, between 0 and 255", "console": "Reading and writing data to the console output.", "console.addListener": "Adds a listener for the log messages", "console.inspect": "Convert any object or value to a string representation", diff --git a/libs/core/_locales/core-strings.json b/libs/core/_locales/core-strings.json index be22fb1780d..8e0cc48e564 100644 --- a/libs/core/_locales/core-strings.json +++ b/libs/core/_locales/core-strings.json @@ -155,6 +155,7 @@ "Math.SQRT2|block": "√2", "Math._constant|block": "$MEMBER", "Math.constrain|block": "constrain %value|between %low|and %high", + "Math.convert|block": "convert $value|from $type", "Math.map|block": "map %value|from low %fromLow|high %fromHigh|to low %toLow|high %toHigh", "Math.randomBoolean|block": "pick random true or false", "Math.randomRange|block": "pick random %min|to %limit", @@ -270,6 +271,10 @@ "TouchTarget.P2|block": "P2", "TouchTargetMode.Capacitive|block": "capacitive", "TouchTargetMode.Resistive|block": "resistive", + "UnitConversion.CelsiusToFahrenheit|block": "celsius to fahrenheit", + "UnitConversion.DegreesToRadians|block": "degrees to radians", + "UnitConversion.FahrenheitToCelsius|block": "fahrenheit to celsius", + "UnitConversion.RadiansToDegrees|block": "radians to degrees", "WaveShape.Noise|block": "noise", "WaveShape.Sawtooth|block": "sawtooth", "WaveShape.Sine|block": "sine", @@ -285,6 +290,7 @@ "basic.showNumber|block": "show|number %number", "basic.showString|block": "show|string %text", "basic|block": "basic", + "colorHelpers|block": "colorHelpers", "console|block": "console", "control.deviceName|block": "device name", "control.deviceSerialNumber|block": "device serial number", @@ -364,7 +370,7 @@ "led.stopAnimation|block": "stop animation", "led.toggle|block": "toggle|x %x|y %y", "led.unplot|block": "unplot|x %x|y %y", - "led|block": "led", + "led|block": "LED", "light|block": "light", "loops.everyInterval|block": "every $interval ms", "loops|block": "loops", @@ -474,6 +480,7 @@ "{id:category}Basic": "Basic", "{id:category}Boolean": "Boolean", "{id:category}Buffer": "Buffer", + "{id:category}ColorHelpers": "ColorHelpers", "{id:category}Console": "Console", "{id:category}Control": "Control", "{id:category}DigitalInOutPin": "DigitalInOutPin", diff --git a/pxtarget.json b/pxtarget.json index 90f4ceeb06a..56ab4375232 100644 --- a/pxtarget.json +++ b/pxtarget.json @@ -367,6 +367,19 @@ 80, 0 ] + }, + "instrument": { + "output": [ + "basic.showNumber", "basic.showString", "basic.showLeds", + "basic.showIcon", "basic.showArrow", "basic.clearScreen", + "led.plot", "led.toggle", "led.unplot", "led.plotBarGraph", + "led.plotBrightness", "led.setBrightness", "led.enable", "led.stopAnimation", + "music.stopAllSounds","music.setBuiltInSpeakerEnabled", + "music.setVolume", "music.ringTone", + "radio.sendNumber", "radio.sendValue", "radio.sendString", + "radio.setGroup", "radio.setTransmitPower", + "pins.digitalWritePin", "pins.analogWritePin" + ] } }, "serial": { @@ -437,7 +450,8 @@ "debugExtensionCode", "bluetoothUartConsole", "bluetoothPartialFlashing", - "forceEnableAiErrorHelp" + "forceEnableAiErrorHelp", + "physicalSimulator" ], "supportedExperiences": [ "code-eval" diff --git a/sim/dalboard.ts b/sim/dalboard.ts index c4af045ef57..1b9b440356d 100644 --- a/sim/dalboard.ts +++ b/sim/dalboard.ts @@ -132,6 +132,9 @@ namespace pxsim { } } + setTitle(title: string) { + this.viewHost.setTitle(title); + } initAsync(msg: SimulatorRunMessage): Promise { super.initAsync(msg); diff --git a/sim/visuals/microbit.ts b/sim/visuals/microbit.ts index 939b41943f7..8ffd6f56a9c 100644 --- a/sim/visuals/microbit.ts +++ b/sim/visuals/microbit.ts @@ -346,6 +346,8 @@ path.sim-board { private leds: SVGElement[]; private microphoneLed: SVGElement; private systemLed: SVGCircleElement; + private title: string | undefined; + private titleElement: SVGTextElement; private antenna: SVGElement; private antennaInitialized = false; private rssi: SVGTextElement; @@ -490,6 +492,7 @@ path.sim-board { this.updateTilt(); this.updateHeading(); this.updateLightLevel(); + this.updateTitleElement() this.updateTemperature(); this.updateButtonAB(); this.updateGestures(); @@ -840,6 +843,11 @@ path.sim-board { svg.animate(this.systemLed, "sim-flash") } } + + public setTitle(title: string) { + this.title = title; + this.buildTitleElement(); + } private lastAntennaFlash: number = 0; public flashAntenna() { @@ -1101,6 +1109,7 @@ path.sim-board { // Order of construction affects tab ordering this.buildLightLevelElement(); + this.buildTitleElement(); this.buildAntennaElement(); this.buildHeadElement(); this.buildThermometerElement(); @@ -1110,6 +1119,18 @@ path.sim-board { this.buildPinElements(); } + // build a name element in the upper left corner of the board + private buildTitleElement() { + if (!this.titleElement) { + this.titleElement = svg.child(this.g, "text", { class: "sim-text", x: 200, y: 30 }) as SVGTextElement; + } + this.titleElement.textContent = this.title || ""; + } + + private updateTitleElement() { + this.buildTitleElement(); + } + private buildAntennaElement() { this.antenna = svg.child(this.g, "g", { class: "sim-antenna-outer" });