Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
207 changes: 207 additions & 0 deletions PSIM_SPEC.md
Original file line number Diff line number Diff line change
@@ -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)
```
Binary file added docs/static/experiments/physicalSimulator.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions libs/core/_locales/core-jsdoc-strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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``.",
Expand Down Expand Up @@ -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",
Expand Down
9 changes: 8 additions & 1 deletion libs/core/_locales/core-strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
16 changes: 15 additions & 1 deletion pxtarget.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -437,7 +450,8 @@
"debugExtensionCode",
"bluetoothUartConsole",
"bluetoothPartialFlashing",
"forceEnableAiErrorHelp"
"forceEnableAiErrorHelp",
"physicalSimulator"
],
"supportedExperiences": [
"code-eval"
Expand Down
3 changes: 3 additions & 0 deletions sim/dalboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ namespace pxsim {
}
}

setTitle(title: string) {
this.viewHost.setTitle(title);
}

initAsync(msg: SimulatorRunMessage): Promise<void> {
super.initAsync(msg);
Expand Down
21 changes: 21 additions & 0 deletions sim/visuals/microbit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,8 @@
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;
Expand Down Expand Up @@ -490,6 +492,7 @@
this.updateTilt();
this.updateHeading();
this.updateLightLevel();
this.updateTitleElement()
this.updateTemperature();
this.updateButtonAB();
this.updateGestures();
Expand Down Expand Up @@ -626,7 +629,7 @@
if (pin.mode & PinFlags.Analog) {
v = Math.floor(100 - (pin.value || 0) / 1023 * 100) + "%";
if (text) text.textContent = (pin.period ? "~" : "") + (pin.value || 0) + "";
ariaValueNow = pin.value ?? 0;

Check failure on line 632 in sim/visuals/microbit.ts

View workflow job for this annotation

GitHub Actions / buildpush

':' expected.

Check failure on line 632 in sim/visuals/microbit.ts

View workflow job for this annotation

GitHub Actions / buildpush

Expression expected.

Check failure on line 632 in sim/visuals/microbit.ts

View workflow job for this annotation

GitHub Actions / buildpush

':' expected.

Check failure on line 632 in sim/visuals/microbit.ts

View workflow job for this annotation

GitHub Actions / buildpush

Expression expected.
}
else if (pin.mode & PinFlags.Digital) {
v = pin.value > 0 ? "0%" : "100%";
Expand Down Expand Up @@ -659,9 +662,9 @@
this.pins[index].setAttribute("aria-valuemin", "0");
this.pins[index].setAttribute("aria-valuemax", pin.mode & PinFlags.Analog ? "1023" : "1");
this.pins[index].setAttribute("aria-orientation", "vertical");
this.pins[index].setAttribute("aria-valuenow", ariaValueNow.toString() ?? "");

Check failure on line 665 in sim/visuals/microbit.ts

View workflow job for this annotation

GitHub Actions / buildpush

':' expected.

Check failure on line 665 in sim/visuals/microbit.ts

View workflow job for this annotation

GitHub Actions / buildpush

Expression expected.

Check failure on line 665 in sim/visuals/microbit.ts

View workflow job for this annotation

GitHub Actions / buildpush

':' expected.

Check failure on line 665 in sim/visuals/microbit.ts

View workflow job for this annotation

GitHub Actions / buildpush

Expression expected.
// Check that the text content isn't just a plain int and only set aria-valuetext if required.
if (text?.textContent && text?.textContent !== parseInt(text?.textContent).toString()) {

Check failure on line 667 in sim/visuals/microbit.ts

View workflow job for this annotation

GitHub Actions / buildpush

':' expected.

Check failure on line 667 in sim/visuals/microbit.ts

View workflow job for this annotation

GitHub Actions / buildpush

Expression expected.

Check failure on line 667 in sim/visuals/microbit.ts

View workflow job for this annotation

GitHub Actions / buildpush

Expression expected.

Check failure on line 667 in sim/visuals/microbit.ts

View workflow job for this annotation

GitHub Actions / buildpush

Expression expected.

Check failure on line 667 in sim/visuals/microbit.ts

View workflow job for this annotation

GitHub Actions / buildpush

':' expected.

Check failure on line 667 in sim/visuals/microbit.ts

View workflow job for this annotation

GitHub Actions / buildpush

Expression expected.

Check failure on line 667 in sim/visuals/microbit.ts

View workflow job for this annotation

GitHub Actions / buildpush

Expression expected.

Check failure on line 667 in sim/visuals/microbit.ts

View workflow job for this annotation

GitHub Actions / buildpush

Expression expected.
this.pins[index].setAttribute("aria-valuetext", text.textContent);
} else {
this.pins[index].removeAttribute("aria-valuetext");
Expand Down Expand Up @@ -840,6 +843,11 @@
svg.animate(this.systemLed, "sim-flash")
}
}

public setTitle(title: string) {
this.title = title;
this.buildTitleElement();
}

private lastAntennaFlash: number = 0;
public flashAntenna() {
Expand Down Expand Up @@ -1101,6 +1109,7 @@

// Order of construction affects tab ordering
this.buildLightLevelElement();
this.buildTitleElement();
this.buildAntennaElement();
this.buildHeadElement();
this.buildThermometerElement();
Expand All @@ -1110,6 +1119,18 @@
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" });

Expand Down
Loading