From 46c18af4616a0b0c85c3e6b0f86ba42b029e489c Mon Sep 17 00:00:00 2001 From: Sam El-Husseini Date: Tue, 11 Jul 2017 11:15:17 +0300 Subject: [PATCH] Initial board SVG and basic simulator --- libs/base/_locales/base-jsdoc-strings.json | 99 ++ libs/base/_locales/base-strings.json | 36 + libs/core/_locales/core-jsdoc-strings.json | 30 + libs/core/_locales/core-strings.json | 28 + libs/core/sim/analogSensor.ts | 81 ++ libs/core/sim/pins.ts | 177 +++ libs/ev3/_locales/ev3-jsdoc-strings.json | 1 + libs/ev3/_locales/ev3-strings.json | 1 + libs/music/_locales/music-jsdoc-strings.json | 28 + libs/music/_locales/music-strings.json | 46 + pxtarget.json | 12 +- sim/dalboard.ts | 197 +++ sim/instructions.ts | 92 ++ sim/public/parts/.gitignore | 5 + sim/public/sim.manifest | 10 + sim/public/siminstructions.html | 201 +++ sim/public/simulator.html | 29 + sim/state/buttons.ts | 18 + sim/state/control.ts | 9 + sim/state/light.ts | 19 + sim/tsconfig.json | 12 + sim/visuals/board.svg | 1178 ++++++++++++++++++ sim/visuals/board.ts | 803 ++++++++++++ sim/visuals/boardsvg.ts | 1178 ++++++++++++++++++ sim/visuals/boardview.ts | 10 + sim/visuals/pincontrol.ts | 92 ++ theme/site/globals/site.variables | 2 +- theme/style.less | 4 + 28 files changed, 4390 insertions(+), 8 deletions(-) create mode 100644 libs/base/_locales/base-jsdoc-strings.json create mode 100644 libs/base/_locales/base-strings.json create mode 100644 libs/core/_locales/core-jsdoc-strings.json create mode 100644 libs/core/_locales/core-strings.json create mode 100644 libs/core/sim/analogSensor.ts create mode 100644 libs/core/sim/pins.ts create mode 100644 libs/ev3/_locales/ev3-jsdoc-strings.json create mode 100644 libs/ev3/_locales/ev3-strings.json create mode 100644 libs/music/_locales/music-jsdoc-strings.json create mode 100644 libs/music/_locales/music-strings.json create mode 100644 sim/dalboard.ts create mode 100644 sim/instructions.ts create mode 100644 sim/public/parts/.gitignore create mode 100644 sim/public/sim.manifest create mode 100644 sim/public/siminstructions.html create mode 100644 sim/public/simulator.html create mode 100644 sim/state/buttons.ts create mode 100644 sim/state/control.ts create mode 100644 sim/state/light.ts create mode 100644 sim/tsconfig.json create mode 100644 sim/visuals/board.svg create mode 100644 sim/visuals/board.ts create mode 100644 sim/visuals/boardsvg.ts create mode 100644 sim/visuals/boardview.ts create mode 100644 sim/visuals/pincontrol.ts diff --git a/libs/base/_locales/base-jsdoc-strings.json b/libs/base/_locales/base-jsdoc-strings.json new file mode 100644 index 00000000..e865a90d --- /dev/null +++ b/libs/base/_locales/base-jsdoc-strings.json @@ -0,0 +1,99 @@ +{ + "Math.abs": "Returns the absolute value of a number (the value without regard to whether it is positive or negative).\nFor example, the absolute value of -5 is the same as the absolute value of 5.", + "Math.abs|param|x": "A numeric expression for which the absolute value is needed.", + "Math.acos": "Returns the arccosine (in radians) of a number", + "Math.acos|param|x": "A number", + "Math.asin": "Returns the arcsine (in radians) of a number", + "Math.asin|param|x": "A number", + "Math.atan": "Returns the arctangent (in radians) of a number", + "Math.atan2": "Returns the arctangent of the quotient of its arguments.", + "Math.atan2|param|x": "A number", + "Math.atan2|param|y": "A number", + "Math.atan|param|x": "A number", + "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.cos": "Returns the cosine of a number.", + "Math.cos|param|x": "An angle in radians", + "Math.exp": "Returns returns ``e^x``.", + "Math.exp|param|x": "A number", + "Math.floor": "Returns the greatest number less than or equal to its numeric argument.", + "Math.floor|param|x": "A numeric expression.", + "Math.icos": "Returns the cosine of an input angle. This is an 8-bit approximation.", + "Math.icos|param|theta": "input angle from 0-255", + "Math.idiv": "Returns the value of integer signed 32 bit division of two numbers.", + "Math.idiv|param|x": "The first number", + "Math.idiv|param|y": "The second number", + "Math.imul": "Returns the value of integer signed 32 bit multiplication of two numbers.", + "Math.imul|param|x": "The first number", + "Math.imul|param|y": "The second number", + "Math.isin": "Returns the sine of an input angle. This is an 8-bit approximation.", + "Math.isin|param|theta": "input angle from 0-255", + "Math.log": "Returns the natural logarithm (base e) of a number.", + "Math.log|param|x": "A number", + "Math.map": "Re-maps a number from one range to another. That is, a value of ``from low`` would get mapped to ``to low``, a value of ``from high`` to ``to high``, values in-between to values in-between, etc.", + "Math.map|param|fromHigh": "the upper bound of the value's current range, eg: 1023", + "Math.map|param|fromLow": "the lower bound of the value's current range", + "Math.map|param|toHigh": "the upper bound of the value's target range, eg: 4", + "Math.map|param|toLow": "the lower bound of the value's target range", + "Math.map|param|value": "value to map in ranges", + "Math.max": "Returns the larger of two supplied numeric expressions.", + "Math.min": "Returns the smaller of two supplied numeric expressions.", + "Math.pow": "Returns the value of a base expression taken to a specified power.", + "Math.pow|param|x": "The base value of the expression.", + "Math.pow|param|y": "The exponent value of the expression.", + "Math.random": "Returns a pseudorandom number between 0 and 1.", + "Math.randomRange": "Returns a pseudorandom number between min and max included. \nIf both numbers are integral, the result is integral.", + "Math.randomRange|param|max": "the upper inclusive bound, eg: 10", + "Math.randomRange|param|min": "the lower inclusive bound, eg: 0", + "Math.round": "Returns a supplied numeric expression rounded to the nearest number.", + "Math.round|param|x": "The value to be rounded to the nearest number.", + "Math.sign": "Returns the sign of the x, indicating whether x is positive, negative or zero.", + "Math.sign|param|x": "The numeric expression to test", + "Math.sin": "Returns the sine of a number.", + "Math.sin|param|x": "An angle in radians", + "Math.sqrt": "Returns the square root of a number.", + "Math.sqrt|param|x": "A numeric expression.", + "Math.tan": "Returns the tangent of a number.", + "Math.tan|param|x": "An angle in radians", + "Math.trunc": "Returns the number with the decimal part truncated.", + "Math.trunc|param|x": "A numeric expression.", + "String.charAt": "Returns the character at the specified index.", + "String.charAt|param|index": "The zero-based index of the desired character.", + "String.charCodeAt": "Returns the Unicode value of the character at the specified location.", + "String.charCodeAt|param|index": "The zero-based index of the desired character. If there is no character at the specified index, NaN is returned.", + "String.compare": "Determines whether relative order of two strings (in ASCII encoding).", + "String.compare|param|that": "String to compare to target string", + "String.concat": "Returns a string that contains the concatenation of two or more strings.", + "String.concat|param|other": "The string to append to the end of the string.", + "String.fromCharCode": "Make a string from the given ASCII character code.", + "String.isEmpty": "Returns a value indicating if the string is empty", + "String.length": "Returns the length of a String object.", + "String.substr": "Return substring of the current string.", + "String.substr|param|length": "number of characters to extract", + "String.substr|param|start": "first character index; can be negative from counting from the end, eg:0", + "control": "Program controls and events.", + "control.assert": "Display specified error code and stop the program.", + "control.deviceSerialNumber": "Derive a unique, consistent serial number of this device from internal data.", + "control.millis": "Gets the number of milliseconds elapsed since power on.", + "control.onEvent": "Run code when a registered event happens.", + "control.onEvent|param|value": "the event value to match", + "control.panic": "Display an error code and stop the program.", + "control.panic|param|code": "an error number to display. eg: 5", + "control.reset": "Reset the device.", + "control.runInBackground": "Run other code in the background.", + "control.waitForEvent": "Blocks the calling thread until the specified event is raised.", + "control.waitMicros": "Block the current fiber for the given microseconds", + "control.waitMicros|param|micros": "number of micro-seconds to wait. eg: 4", + "loops.forever": "Repeats the code forever in the background. On each iteration, allows other codes to run.", + "loops.pause": "Pause for the specified time in milliseconds", + "loops.pause|param|ms": "how long to pause for, eg: 100, 200, 500, 1000, 2000", + "serial": "Reading and writing data over a serial connection.", + "serial.writeBuffer": "Send a buffer across the serial connection.", + "serial.writeLine": "Write a line of text to the serial port.", + "serial.writeNumber": "Write a number to the serial port.", + "serial.writeString": "Write some text to the serial port.", + "serial.writeValue": "Write a name:value pair as a line of text to the serial port.", + "serial.writeValue|param|name": "name of the value stream, eg: x", + "serial.writeValue|param|value": "to write" +} \ No newline at end of file diff --git a/libs/base/_locales/base-strings.json b/libs/base/_locales/base-strings.json new file mode 100644 index 00000000..258f4397 --- /dev/null +++ b/libs/base/_locales/base-strings.json @@ -0,0 +1,36 @@ +{ + "Math.constrain|block": "constrain %value|between %low|and %high", + "Math.map|block": "map %value|from low %fromLow|from high %fromHigh|to low %toLow|to high %toHigh", + "Math.randomRange|block": "pick random %min|to %limit", + "Math|block": "Math", + "String.charAt|block": "char from %this=text|at %pos", + "String.compare|block": "compare %this=text| to %that", + "String.fromCharCode|block": "text from char code %code", + "String.length|block": "length of %VALUE", + "String.substr|block": "substring of %this=text|from %start|of length %length", + "String|block": "String", + "control.deviceSerialNumber|block": "device serial number", + "control.millis|block": "millis (ms)", + "control.onEvent|block": "on event|from %src|with value %value", + "control.panic|block": "panic %code", + "control.reset|block": "reset", + "control.runInBackground|block": "run in background", + "control.waitForEvent|block": "wait for event|from %src|with value %value", + "control.waitMicros|block": "wait (µs)%micros", + "control|block": "control", + "loops.forever|block": "forever", + "loops.pause|block": "pause (ms) %pause", + "loops|block": "loops", + "serial.writeBuffer|block": "serial|write buffer %buffer", + "serial.writeLine|block": "serial|write line %text", + "serial.writeNumber|block": "serial|write number %value", + "serial.writeString|block": "serial|write string %text", + "serial.writeValue|block": "serial|write value %name|= %value", + "serial|block": "serial", + "{id:category}Control": "Control", + "{id:category}Loops": "Loops", + "{id:category}Math": "Math", + "{id:category}Serial": "Serial", + "{id:category}String": "String", + "{id:category}Text": "Text" +} \ No newline at end of file diff --git a/libs/core/_locales/core-jsdoc-strings.json b/libs/core/_locales/core-jsdoc-strings.json new file mode 100644 index 00000000..ee6004f7 --- /dev/null +++ b/libs/core/_locales/core-jsdoc-strings.json @@ -0,0 +1,30 @@ +{ + "control": "Program controls and events.", + "control.allocateNotifyEvent": "Allocates the next user notification event", + "control.deviceFirmwareVersion": "Determine the version of system software currently running.", + "control.dmesg": "Write data to DMESG debugging buffer.", + "control.mmap": "Create new file mapping in memory", + "control.raiseEvent": "Announce that an event happened to registered handlers.", + "control.raiseEvent|param|src": "ID of the Component that generated the event", + "control.raiseEvent|param|value": "Component specific code indicating the cause of the event.", + "input.Button": "Generic button class, for device buttons and sensors.", + "input.buttonDown": "Down button.", + "input.buttonEnter": "Enter button.", + "input.buttonLeft": "Left button.", + "input.buttonRight": "Right button.", + "input.buttonUp": "Up button.", + "input.remoteBottomLeft": "Remote bottom-left button.", + "input.remoteBottomRight": "Remote bottom-right button.", + "input.remoteCenter": "Remote beacon (center) button.", + "input.remoteTopLeft": "Remote top-left button.", + "input.remoteTopRight": "Remote top-right button.", + "output.createBuffer": "Create a new zero-initialized buffer.", + "output.createBuffer|param|size": "number of bytes in the buffer", + "output.setLights": "Set lights.", + "screen.clear": "Clear screen and reset font to normal.", + "screen.drawText": "Draw text.", + "screen.scroll": "Scroll screen vertically.", + "screen.setFont": "Set font for drawText()", + "serial": "Reading and writing data over a serial connection.", + "serial.writeDmesg": "Send DMESG debug buffer over serial." +} \ No newline at end of file diff --git a/libs/core/_locales/core-strings.json b/libs/core/_locales/core-strings.json new file mode 100644 index 00000000..5744a682 --- /dev/null +++ b/libs/core/_locales/core-strings.json @@ -0,0 +1,28 @@ +{ + "ButtonEvent.Click|block": "click", + "ButtonEvent.Down|block": "down", + "ButtonEvent.LongClick|block": "long click", + "ButtonEvent.Up|block": "up", + "control.raiseEvent|block": "raise event|from %src|with value value", + "control|block": "control", + "input.buttonDown|block": "button down", + "input.buttonEnter|block": "button enter", + "input.buttonLeft|block": "button left", + "input.buttonRight|block": "button right", + "input.buttonUp|block": "button up", + "input.remoteBottomLeft|block": "remote bottom-left", + "input.remoteBottomRight|block": "remote bottom-right", + "input.remoteCenter|block": "remote center", + "input.remoteTopLeft|block": "remote top-left", + "input.remoteTopRight|block": "remote top-right", + "input|block": "input", + "output.setLights|block": "set lights %pattern", + "output|block": "output", + "screen|block": "screen", + "serial|block": "serial", + "{id:category}Control": "Control", + "{id:category}Input": "Input", + "{id:category}Output": "Output", + "{id:category}Screen": "Screen", + "{id:category}Serial": "Serial" +} \ No newline at end of file diff --git a/libs/core/sim/analogSensor.ts b/libs/core/sim/analogSensor.ts new file mode 100644 index 00000000..39efaebb --- /dev/null +++ b/libs/core/sim/analogSensor.ts @@ -0,0 +1,81 @@ +namespace pxsim { + enum ThresholdState { + High, + Low, + Normal + } + + export class AnalogSensorState { + public sensorUsed: boolean = false; + + private level: number; + private state = ThresholdState.Normal; + + constructor(public id: number, private min = 0, private max = 255, private lowThreshold = 64, private highThreshold = 192) { + this.level = Math.ceil((max - min) / 2); + } + + public setUsed() { + if (!this.sensorUsed) { + this.sensorUsed = true; + runtime.queueDisplayUpdate(); + } + } + + public setLevel(level: number) { + this.level = this.clampValue(level); + + if (this.level >= this.highThreshold) { + this.setState(ThresholdState.High); + } + else if (this.level <= this.lowThreshold) { + this.setState(ThresholdState.Low); + } + else { + this.setState(ThresholdState.Normal); + } + } + + public getLevel(): number { + return this.level; + } + + public setLowThreshold(value: number) { + this.lowThreshold = this.clampValue(value); + this.highThreshold = Math.max(this.lowThreshold + 1, this.highThreshold); + } + + public setHighThreshold(value: number) { + this.highThreshold = this.clampValue(value); + this.lowThreshold = Math.min(this.highThreshold - 1, this.lowThreshold); + } + + private clampValue(value: number) { + if (value < this.min) { + return this.min; + } + else if (value > this.max) { + return this.max; + } + return value; + } + + private setState(state: ThresholdState) { + if (this.state === state) { + return; + } + + this.state = state; + switch (state) { + case ThresholdState.High: + board().bus.queue(this.id, DAL.ANALOG_THRESHOLD_HIGH); + break; + case ThresholdState.Low: + board().bus.queue(this.id, DAL.ANALOG_THRESHOLD_LOW); + break; + case ThresholdState.Normal: + break; + } + } + } +} \ No newline at end of file diff --git a/libs/core/sim/pins.ts b/libs/core/sim/pins.ts new file mode 100644 index 00000000..2ba0961c --- /dev/null +++ b/libs/core/sim/pins.ts @@ -0,0 +1,177 @@ + +namespace pxsim.pins { + export class CommonPin extends Pin { + used: boolean; + } + + export class DigitalPin extends CommonPin { + } + + export class AnalogPin extends CommonPin { + + } + + export function markUsed(name: CommonPin) { + if (!name.used) { + name.used = true; + runtime.queueDisplayUpdate(); + } + } +} + +namespace pxsim.DigitalPinMethods { + export function digitalRead(name: pins.DigitalPin): number { + return name.digitalReadPin(); + } + + /** + * Set a pin or connector value to either 0 or 1. + * @param value value to set on the pin, 1 eg,0 + */ + export function digitalWrite(name: pins.DigitalPin, value: number): void { + name.digitalWritePin(value); + } + + /** + * Configures this pin to a digital input, and generates events where the timestamp is the duration + * that this pin was either ``high`` or ``low``. + */ + export function onPulsed(name: pins.DigitalPin, pulse: number, body: RefAction): void { + // TODO + } + + /** + * Returns the duration of a pulse in microseconds + * @param value the value of the pulse (default high) + * @param maximum duration in micro-seconds + */ + export function pulseIn(name: pins.DigitalPin, pulse: number, maxDuration = 2000000): number { + // TODO + return 500; + } + + /** + * Configures the pull of this pin. + * @param pull one of the mbed pull configurations: PullUp, PullDown, PullNone + */ + export function setPull(name: pins.DigitalPin, pull: number): void { + name.setPull(pull); + } + + /** + * Do something when a pin is pressed. + * @param body the code to run when the pin is pressed + */ + export function onPressed(name: pins.DigitalPin, body: RefAction): void { + } + + /** + * Do something when a pin is released. + * @param body the code to run when the pin is released + */ + export function onReleased(name: pins.DigitalPin, body: RefAction): void { + } + + /** + * Get the pin state (pressed or not). Requires to hold the ground to close the circuit. + * @param name pin used to detect the touch + */ + export function isPressed(name: pins.DigitalPin): boolean { + return name.isTouched(); + } +} + +namespace pxsim.AnalogPinMethods { + /** + * Read the connector value as analog, that is, as a value comprised between 0 and 1023. + */ + export function analogRead(name: pins.AnalogPin): number { + pins.markUsed(name); + return name.analogReadPin(); + } + + /** + * Set the connector value as analog. Value must be comprised between 0 and 1023. + * @param value value to write to the pin between ``0`` and ``1023``. eg:1023,0 + */ + export function analogWrite(name: pins.AnalogPin, value: number): void { + pins.markUsed(name); + name.analogWritePin(value); + + } + + /** + * Configures the Pulse-width modulation (PWM) of the analog output to the given value in + * **microseconds** or `1/1000` milliseconds. + * If this pin is not configured as an analog output (using `analog write pin`), the operation has + * no effect. + * @param micros period in micro seconds. eg:20000 + */ + export function analogSetPeriod(name: pins.AnalogPin, micros: number): void { + pins.markUsed(name); + name.analogSetPeriod(micros); + } + + /** + * Writes a value to the servo, controlling the shaft accordingly. On a standard servo, this will + * set the angle of the shaft (in degrees), moving the shaft to that orientation. On a continuous + * rotation servo, this will set the speed of the servo (with ``0`` being full-speed in one + * direction, ``180`` being full speed in the other, and a value near ``90`` being no movement). + * @param value angle or rotation speed, eg:180,90,0 + */ + export function servoWrite(name: pins.AnalogPin, value: number): void { + pins.markUsed(name); + name.servoWritePin(value); + } + + /** + * Configures this IO pin as an analog/pwm output, configures the period to be 20 ms, and sets the + * pulse width, based on the value it is given **microseconds** or `1/1000` milliseconds. + * @param micros pulse duration in micro seconds, eg:1500 + */ + export function servoSetPulse(name: pins.AnalogPin, micros: number): void { + pins.markUsed(name); + // TODO fix pxt + // name.servoSetPulse(micros); + } +} + +namespace pxsim.PwmPinMethods { + export function analogSetPeriod(name: pins.AnalogPin, micros: number): void { + name.analogSetPeriod(micros); + } + + export function servoWrite(name: pins.AnalogPin, value: number): void { + name.servoWritePin(value); + } + + export function servoSetPulse(name: pins.AnalogPin, micros: number): void { + name.servoSetPulse(name.id, micros); + } +} + +namespace pxsim.pins { + export function pulseDuration(): number { + // bus last event timestamp + return 500; + } + + export function createBuffer(sz: number) { + return pxsim.BufferMethods.createBuffer(sz) + } + + export function spiWrite(value: number): number { + // TODO + return 0; + } + + export function i2cReadBuffer(address: number, size: number, repeat?: boolean): RefBuffer { + // fake reading zeros + return createBuffer(size) + } + + export function i2cWriteBuffer(address: number, buf: RefBuffer, repeat?: boolean): void { + // fake - noop + } +} + diff --git a/libs/ev3/_locales/ev3-jsdoc-strings.json b/libs/ev3/_locales/ev3-jsdoc-strings.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/libs/ev3/_locales/ev3-jsdoc-strings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/libs/ev3/_locales/ev3-strings.json b/libs/ev3/_locales/ev3-strings.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/libs/ev3/_locales/ev3-strings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/libs/music/_locales/music-jsdoc-strings.json b/libs/music/_locales/music-jsdoc-strings.json new file mode 100644 index 00000000..2ea11296 --- /dev/null +++ b/libs/music/_locales/music-jsdoc-strings.json @@ -0,0 +1,28 @@ +{ + "music": "Generation of music tones.", + "music.beat": "Return the duration of a beat in milliseconds (the beat fraction).", + "music.beat|param|fraction": "the fraction of the current whole note, eg: BeatFraction.Half", + "music.changeTempoBy": "Change the tempo up or down by some amount of beats per minute (bpm).", + "music.changeTempoBy|param|bpm": "The change in beats per minute to the tempo, eg: 20", + "music.noteFrequency": "Get the frequency of a note.", + "music.noteFrequency|param|name": "the note name, eg: Note.C", + "music.playSound": "Start playing a sound and don't wait for it to finish.\nNotes are expressed as a string of characters with this format: NOTE[octave][:duration]", + "music.playSoundUntilDone": "Play a sound and wait until the sound is done.\nNotes are expressed as a string of characters with this format: NOTE[octave][:duration]", + "music.playSoundUntilDone|param|sound": "the melody to play, eg: 'g5:1'", + "music.playSound|param|sound": "the melody to play, eg: 'g5:1'", + "music.playTone": "Play a tone through the speaker for some amount of time.", + "music.playTone|param|frequency": "pitch of the tone to play in Hertz (Hz)", + "music.playTone|param|ms": "tone duration in milliseconds (ms)", + "music.rest": "Rest, or play silence, for some time (in milleseconds).", + "music.rest|param|ms": "rest duration in milliseconds (ms)", + "music.ringTone": "Play a tone.", + "music.ringTone|param|frequency": "pitch of the tone to play in Hertz (Hz)", + "music.setTempo": "Set the tempo a number of beats per minute (bpm).", + "music.setTempo|param|bpm": "The new tempo in beats per minute, eg: 120", + "music.setVolume": "Set the output volume of the sound synthesizer.", + "music.setVolume|param|volume": "the volume 0...256, eg: 128", + "music.sounds": "Get the melody string for a built-in melody.", + "music.sounds|param|name": "the note name, eg: Note.C", + "music.stopAllSounds": "Stop all sounds from playing.", + "music.tempo": "Return the tempo in beats per minute (bpm).\nTempo is the speed (bpm = beats per minute) at which notes play. The larger the tempo value, the faster the notes will play." +} \ No newline at end of file diff --git a/libs/music/_locales/music-strings.json b/libs/music/_locales/music-strings.json new file mode 100644 index 00000000..ee0d0e9d --- /dev/null +++ b/libs/music/_locales/music-strings.json @@ -0,0 +1,46 @@ +{ + "BeatFraction.Breve|block": "4", + "BeatFraction.Double|block": "2", + "BeatFraction.Eighth|block": "1/8", + "BeatFraction.Half|block": "1/2", + "BeatFraction.Quarter|block": "1/4", + "BeatFraction.Sixteenth|block": "1/16", + "BeatFraction.Whole|block": "1", + "Note.CSharp3|block": "C#3", + "Note.CSharp4|block": "C#4", + "Note.CSharp5|block": "C#5", + "Note.CSharp|block": "C#", + "Note.FSharp3|block": "F#3", + "Note.FSharp4|block": "F#4", + "Note.FSharp5|block": "F#5", + "Note.FSharp|block": "F#", + "Note.GSharp3|block": "G#3", + "Note.GSharp4|block": "G#4", + "Note.GSharp5|block": "G#5", + "Note.GSharp|block": "G#", + "SoundOutputDestination.Pin|block": "pin", + "SoundOutputDestination.Speaker|block": "speaker", + "Sounds.BaDing|block": "ba ding", + "Sounds.JumpDown|block": "jump down", + "Sounds.JumpUp|block": "jump up", + "Sounds.MagicWand|block": "magic wand", + "Sounds.PowerDown|block": "power down", + "Sounds.PowerUp|block": "power up", + "Sounds.Siren|block": "siren", + "Sounds.Wawawawaa|block": "wawawawaa", + "music.beat|block": "%fraction|beat", + "music.changeTempoBy|block": "change tempo by (bpm)|%value", + "music.noteFrequency|block": "%note", + "music.playSoundUntilDone|block": "play sound %sound=music_sounds|until done", + "music.playSound|block": "play sound %sound=music_sounds", + "music.playTone|block": "play tone|at %note=device_note|for %duration=device_beat", + "music.rest|block": "rest|for %duration=device_beat", + "music.ringTone|block": "ring tone|at %note=device_note", + "music.setTempo|block": "set tempo to (bpm)|%value", + "music.setVolume|block": "set volume %volume", + "music.sounds|block": "%name", + "music.stopAllSounds|block": "stop all sounds", + "music.tempo|block": "tempo (bpm)", + "music|block": "music", + "{id:category}Music": "Music" +} \ No newline at end of file diff --git a/pxtarget.json b/pxtarget.json index 6b9f1792..250107a9 100644 --- a/pxtarget.json +++ b/pxtarget.json @@ -13,14 +13,14 @@ "libs/ev3" ], "simulator": { - "headless": true - }, - "DISABLED-simulator": { "autoRun": true, "streams": true, - "aspectRatio": 1.0, + "aspectRatio": 0.67, "parts": false, - "enableTrace": true + "enableTrace": true, + "boardDefinition": { + "visual": "ev3" + } }, "cloud": { "workspace": false, @@ -118,8 +118,6 @@ "invertedMonaco": false, "monacoToolbox": true, "invertedToolbox": true, - "simAnimationEnter": "roll in", - "simAnimationExit": "roll out", "exampleGallery": "examples", "hasAudio": true, "usbHelp": [], diff --git a/sim/dalboard.ts b/sim/dalboard.ts new file mode 100644 index 00000000..726d002b --- /dev/null +++ b/sim/dalboard.ts @@ -0,0 +1,197 @@ +/// +/// +/// + +namespace pxsim { + export enum CPlayPinName { + A0, + A1, + A2, + A3, + A4, + A5, + A6, + A7, + A8, + A9, + D4, + D5, + D6, + D7, + D8, + D13 + } + + export class DalBoard extends CoreBoard implements + AccelerometerBoard, + CommonBoard, + LightBoard, + LightSensorBoard, + MicrophoneBoard, + MusicBoard, + SlideSwitchBoard, + TemperatureBoard, + InfraredBoard, + CapTouchBoard { + // state & update logic for component services + neopixelState: CommonNeoPixelState; + buttonState: EV3ButtonState; + slideSwitchState: SlideSwitchState; + lightSensorState: AnalogSensorState; + thermometerState: AnalogSensorState; + thermometerUnitState: number; + microphoneState: AnalogSensorState; + edgeConnectorState: EdgeConnectorState; + capacitiveSensorState: CapacitiveSensorState; + accelerometerState: AccelerometerState; + audioState: AudioState; + touchButtonState: TouchButtonState; + irState: InfraredState; + lightState: EV3LightState; + + view: SVGSVGElement; + + constructor() { + super() + + this.bus.setNotify(DAL.DEVICE_ID_NOTIFY, DAL.DEVICE_ID_NOTIFY_ONE); + + //components + + this.builtinParts["buttons"] = this.buttonState = new EV3ButtonState(); + this.builtinParts["light"] = this.lightState = new EV3LightState(); + /*this.builtinParts["neopixel"] = this.neopixelState = new CommonNeoPixelState(); + this.builtinParts["buttonpair"] = this.buttonState = new CommonButtonState(); + + this.builtinParts["switch"] = this.slideSwitchState = new SlideSwitchState(); + this.builtinParts["audio"] = this.audioState = new AudioState(); + this.builtinParts["lightsensor"] = this.lightSensorState = new AnalogSensorState(DAL.DEVICE_ID_LIGHT_SENSOR, 0, 255); + this.builtinParts["thermometer"] = this.thermometerState = new AnalogSensorState(DAL.DEVICE_ID_THERMOMETER, -5, 50); + this.builtinParts["soundsensor"] = this.microphoneState = new AnalogSensorState(DAL.DEVICE_ID_TOUCH_SENSOR + 1, 0, 255); + this.builtinParts["capacitivesensor"] = this.capacitiveSensorState = new CapacitiveSensorState({ + 0: 0, + 1: 1, + 2: 2, + 3: 3, + 6: 4, + 9: 5, + 10: 6, + 12: 7 + }); + + this.builtinParts["accelerometer"] = this.accelerometerState = new AccelerometerState(runtime); + this.builtinParts["edgeconnector"] = this.edgeConnectorState = new EdgeConnectorState({ + pins: [ + pxsim.CPlayPinName.A0, + pxsim.CPlayPinName.A1, + pxsim.CPlayPinName.A2, + pxsim.CPlayPinName.A3, + pxsim.CPlayPinName.A4, + pxsim.CPlayPinName.A5, + pxsim.CPlayPinName.A6, + pxsim.CPlayPinName.A7, + pxsim.CPlayPinName.A8, + pxsim.CPlayPinName.A9, + pxsim.CPlayPinName.D4, + pxsim.CPlayPinName.D5, + pxsim.CPlayPinName.D6, + pxsim.CPlayPinName.D7, + pxsim.CPlayPinName.D8, + pxsim.CPlayPinName.D13 + ] + }); + this.builtinParts["microservo"] = this.edgeConnectorState; + + this.builtinVisuals["microservo"] = () => new visuals.MicroServoView(); + this.builtinPartVisuals["microservo"] = (xy: visuals.Coord) => visuals.mkMicroServoPart(xy); + this.touchButtonState = new TouchButtonState([ + pxsim.CPlayPinName.A1, + pxsim.CPlayPinName.A2, + pxsim.CPlayPinName.A3, + pxsim.CPlayPinName.A4, + pxsim.CPlayPinName.A5, + pxsim.CPlayPinName.A6, + pxsim.CPlayPinName.A7 + ]); + + this.builtinParts["ir"] = this.irState = new InfraredState();*/ + } + + receiveMessage(msg: SimulatorMessage) { + if (!runtime || runtime.dead) return; + + switch (msg.type || "") { + case "eventbus": { + let ev = msg; + this.bus.queue(ev.id, ev.eventid, ev.value); + break; + } + case "serial": { + let data = (msg).data || ""; + // TODO + break; + } + case "irpacket": { + let ev = msg; + this.irState.receive(new RefBuffer(ev.packet)); + break; + } + } + } + + initAsync(msg: SimulatorRunMessage): Promise { + super.initAsync(msg); + + const options = (msg.options || {}) as pxt.RuntimeOptions; + + const boardDef = msg.boardDefinition; + const cmpsList = msg.parts; + const cmpDefs = msg.partDefinitions || {}; + const fnArgs = msg.fnArgs; + + const opts: visuals.BoardHostOpts = { + state: this, + boardDef: boardDef, + partsList: cmpsList, + partDefs: cmpDefs, + fnArgs: fnArgs, + maxWidth: "100%", + maxHeight: "100%", + }; + const viewHost = new visuals.BoardHost(pxsim.visuals.mkBoardView({ + visual: boardDef.visual + }), opts); + + document.body.innerHTML = ""; // clear children + document.body.appendChild(this.view = viewHost.getView() as SVGSVGElement); + + return Promise.resolve(); + } + + screenshot(): string { + return svg.toDataUri(new XMLSerializer().serializeToString(this.view)); + } + + defaultNeopixelPin() { + return this.edgeConnectorState.getPin(CPlayPinName.D8); + } + + getDefaultPitchPin() { + return this.edgeConnectorState.getPin(CPlayPinName.D6); + } + } + + export function initRuntimeWithDalBoard() { + U.assert(!runtime.board); + let b = new DalBoard(); + runtime.board = b; + runtime.postError = (e) => { + // TODO + runtime.updateDisplay(); + } + } + + if (!pxsim.initCurrentRuntime) { + pxsim.initCurrentRuntime = initRuntimeWithDalBoard; + } +} \ No newline at end of file diff --git a/sim/instructions.ts b/sim/instructions.ts new file mode 100644 index 00000000..e3fb8a76 --- /dev/null +++ b/sim/instructions.ts @@ -0,0 +1,92 @@ +/// +/// +/// + +//HACK: allows instructions.html to access pxtblocks without requiring simulator.html to import blocks as well +if (!(window).pxt) (window).pxt = {}; +import pxtrunner = pxt.runner; +import pxtdocs = pxt.docs; + +namespace pxsim.instructions { + export function drawInstructions() { + pxsim.visuals.mkBoardView = (opts: pxsim.visuals.BoardViewOptions): pxsim.visuals.BoardView => { + return new visuals.EV3BoardSvg({ + runtime: runtime, + theme: visuals.randomTheme(), + disableTilt: false, + wireframe: opts.wireframe, + }); + } + + let getQsVal = parseQueryString(); + + //project name + let name = getQsVal("name") || "Untitled"; + + // board def + const boardDef = JSON.parse(getQsVal("board")) as pxsim.BoardDefinition; + + //parts list + let parts = (getQsVal("parts") || "").split(" "); + parts.sort(); + + // parts definitions + let partDefinitions = JSON.parse(getQsVal("partdefs") || "{}") as pxsim.Map + + //fn args + let fnArgs = JSON.parse((getQsVal("fnArgs") || "{}")); + + //project code + let tsCode = getQsVal("code"); + let tsPackage = getQsVal("package") || ""; + let codeSpinnerDiv = document.getElementById("proj-code-spinner"); + let codeContainerDiv = document.getElementById("proj-code-container"); + if (tsCode) { + //we use the docs renderer to decompile the code to blocks and render it + //TODO: render the blocks code directly + let md = + `\`\`\`blocks +${tsCode} +\`\`\` +\`\`\`package +${tsPackage} +\`\`\` +` + + pxtdocs.requireMarked = function () { return (window).marked; } + pxtrunner.renderMarkdownAsync(codeContainerDiv, md) + .done(function () { + let codeSvg = $("#proj-code-container svg"); + if (codeSvg.length > 0) { + //code rendered successfully as blocks + codeSvg.css("width", "inherit"); + codeSvg.css("height", "inherit"); + //takes the svg out of the wrapper markdown + codeContainerDiv.innerHTML = ""; + codeContainerDiv.appendChild(codeSvg[0]); + } else { + //code failed to convert to blocks, display as typescript instead + codeContainerDiv.innerText = tsCode; + } + $(codeContainerDiv).show(); + $(codeSpinnerDiv).hide(); + }); + } + + + if (name) + $("#proj-title").text(name); + + //init runtime + if (!pxsim.initCurrentRuntime) + pxsim.initCurrentRuntime = initRuntimeWithDalBoard; + + renderParts({ + name, + boardDef, + parts, + partDefinitions, + fnArgs + }) + } +} \ No newline at end of file diff --git a/sim/public/parts/.gitignore b/sim/public/parts/.gitignore new file mode 100644 index 00000000..74a7a76e --- /dev/null +++ b/sim/public/parts/.gitignore @@ -0,0 +1,5 @@ +# don't check in until OSS request is approved +sparkfun-* +raspberrypi-* +arduino-* +max6675* diff --git a/sim/public/sim.manifest b/sim/public/sim.manifest new file mode 100644 index 00000000..bfa592ca --- /dev/null +++ b/sim/public/sim.manifest @@ -0,0 +1,10 @@ +CACHE MANIFEST + +CACHE: +/cdn/bluebird.min.js +/cdn/pxtsim.js +/sim/common-sim.js +/sim/sim.js + +NETWORK: +* diff --git a/sim/public/siminstructions.html b/sim/public/siminstructions.html new file mode 100644 index 00000000..e5fbdd60 --- /dev/null +++ b/sim/public/siminstructions.html @@ -0,0 +1,201 @@ + + + + + + Assembly Instructions + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + +
+

+ + +
+ +
+
+
+
+ + \ No newline at end of file diff --git a/sim/public/simulator.html b/sim/public/simulator.html new file mode 100644 index 00000000..28d631bb --- /dev/null +++ b/sim/public/simulator.html @@ -0,0 +1,29 @@ + + + + + + EV3 Simulator + + + + + + + + + diff --git a/sim/state/buttons.ts b/sim/state/buttons.ts new file mode 100644 index 00000000..7a4cad88 --- /dev/null +++ b/sim/state/buttons.ts @@ -0,0 +1,18 @@ +namespace pxsim { + + export class EV3ButtonState extends CommonButtonState{ + + constructor() { + super(); + this.buttons = [ + new CommonButton(DAL.BUTTON_ID_UP), + new CommonButton(DAL.BUTTON_ID_ENTER), + new CommonButton(DAL.BUTTON_ID_DOWN), + new CommonButton(DAL.BUTTON_ID_RIGHT), + new CommonButton(DAL.BUTTON_ID_LEFT), + new CommonButton(DAL.BUTTON_ID_ESCAPE), + new CommonButton(DAL.BUTTON_ID_ALL) + ]; + } + } +} \ No newline at end of file diff --git a/sim/state/control.ts b/sim/state/control.ts new file mode 100644 index 00000000..7abbea76 --- /dev/null +++ b/sim/state/control.ts @@ -0,0 +1,9 @@ +/// + +namespace pxsim.control { + + export function mmap(filename: string, size: number, offset: number): void { + + } + +} \ No newline at end of file diff --git a/sim/state/light.ts b/sim/state/light.ts new file mode 100644 index 00000000..7ac7f1ed --- /dev/null +++ b/sim/state/light.ts @@ -0,0 +1,19 @@ +namespace pxsim { + + export class EV3LightState { + lightPattern: number; + + constructor() { + this.lightPattern = 0; + } + } +} + +namespace pxsim.output { + + export function setLights(pattern: number){ + const lightState = (board() as DalBoard).lightState; + lightState.lightPattern = pattern; + runtime.queueDisplayUpdate(); + } +} \ No newline at end of file diff --git a/sim/tsconfig.json b/sim/tsconfig.json new file mode 100644 index 00000000..007366f2 --- /dev/null +++ b/sim/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es5", + "noImplicitAny": true, + "noImplicitReturns": true, + "declaration": true, + "out": "../built/sim.js", + "rootDir": ".", + "newLine": "LF", + "sourceMap": false + } +} diff --git a/sim/visuals/board.svg b/sim/visuals/board.svg new file mode 100644 index 00000000..50f13414 --- /dev/null +++ b/sim/visuals/board.svg @@ -0,0 +1,1178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sim/visuals/board.ts b/sim/visuals/board.ts new file mode 100644 index 00000000..3e1b29ff --- /dev/null +++ b/sim/visuals/board.ts @@ -0,0 +1,803 @@ +namespace pxsim.visuals { + const MB_STYLE = ` + svg.sim { + margin-bottom:1em; + } + svg.sim.grayscale { + -moz-filter: grayscale(1); + -webkit-filter: grayscale(1); + filter: grayscale(1); + } + .sim-button { + cursor: pointer; + } + .sim-button:hover { + stroke-width: 2px !important; + stroke: white !important; + } + + .sim-systemled { + fill:#333; + stroke:#555; + stroke-width: 1px; + } + + .sim-light-level-button { + stroke:#f1c40f; + stroke-width: 1px; + } + + .sim-pin-level-button { + stroke:darkorange; + stroke-width: 1px; + } + + .sim-sound-level-button { + stroke:#7f8c8d; + stroke-width: 1px; + } + + .sim-antenna { + stroke:#555; + stroke-width: 2px; + } + + .sim-text { + font-family:"Lucida Console", Monaco, monospace; + font-size:8px; + fill:#fff; + pointer-events: none; user-select: none; + } + .sim-text.small { + font-size:6px; + } + .sim-text.inverted { + fill:#000; + } + + .sim-text-pin { + font-family:"Lucida Console", Monaco, monospace; + font-size:5px; + fill:#fff; + pointer-events: none; + } + + .sim-thermometer { + stroke:#aaa; + stroke-width: 1px; + } + + #rgbledcircle:hover { + r:8px; + } + + #SLIDE_HOVER { + cursor: pointer; + } + .sim-slide-switch:hover #SLIDE_HOVER { + stroke:orange !important; + stroke-width: 1px; + } + + .sim-slide-switch-inner.on { + fill:#ff0000 !important; + } + + /* animations */ + .sim-theme-glow { + animation-name: sim-theme-glow-animation; + animation-timing-function: ease-in-out; + animation-direction: alternate; + animation-iteration-count: infinite; + animation-duration: 1.25s; + } + @keyframes sim-theme-glow-animation { + from { opacity: 1; } + to { opacity: 0.75; } + } + + .sim-flash { + animation-name: sim-flash-animation; + animation-duration: 0.1s; + } + + @keyframes sim-flash-animation { + from { fill: yellow; } + to { fill: default; } + } + + .sim-flash-stroke { + z-index: 0; + animation-name: sim-flash-stroke-animation; + animation-duration: 0.4s; + animation-timing-function: ease-in; + } + + @keyframes sim-flash-stroke-animation { + from { stroke: yellow; } + to { stroke: default; } + } + + + .sim-sound-stroke { + animation-name: sim-sound-stroke-animation; + animation-duration: 0.4s; + } + + @keyframes sim-sound-stroke-animation { + from { stroke: yellow; } + to { stroke: default; } + } + + /* wireframe */ + .sim-wireframe * { + fill: none; + stroke: black; + } + .sim-wireframe .sim-display, + .sim-wireframe .sim-led, + .sim-wireframe .sim-led-back, + .sim-wireframe .sim-head, + .sim-wireframe .sim-theme, + .sim-wireframe .sim-button-group, + .sim-wireframe .sim-button-label, + .sim-wireframe .sim-button, + .sim-wireframe .sim-text-pin + { + visibility: hidden; + } + .sim-wireframe .sim-label + { + stroke: none; + fill: #777; + } + .sim-wireframe .sim-board { + stroke-width: 2px; + } + `; + + const pinNames: { 'name': string, 'touch': number, 'text': any, 'id'?: number, tooltip?: string }[] = [ + { 'name': "PIN_A0", 'touch': 0, 'text': null, 'id': pxsim.CPlayPinName.A0, tooltip: "A0 - Speaker" }, + { 'name': "PIN_A1", 'touch': 1, 'text': null, 'id': pxsim.CPlayPinName.A1, tooltip: "~A1" }, + { 'name': "PIN_A2", 'touch': 1, 'text': null, 'id': pxsim.CPlayPinName.A2, tooltip: "~A2" }, + { 'name': "PIN_A3", 'touch': 1, 'text': null, 'id': pxsim.CPlayPinName.A3, tooltip: "~A3" }, + { 'name': "PIN_A4", 'touch': 1, 'text': null, 'id': pxsim.CPlayPinName.A4, tooltip: "A4 - SCL" }, + { 'name': "PIN_A5", 'touch': 1, 'text': null, 'id': pxsim.CPlayPinName.A5, tooltip: "A5 - SDA" }, + { 'name': "PIN_A6", 'touch': 1, 'text': null, 'id': pxsim.CPlayPinName.A6, tooltip: "A6 - RX" }, + { 'name': "PIN_A7", 'touch': 1, 'text': null, 'id': pxsim.CPlayPinName.A7, tooltip: "A7 - TX" }, + { 'name': "GND_0", 'touch': 0, 'text': null, tooltip: "Ground" }, + { 'name': "GND_1", 'touch': 0, 'text': null, tooltip: "Ground" }, + { 'name': "GND_2", 'touch': 0, 'text': null, tooltip: "Ground" }, + { 'name': "VBATT", 'touch': 0, 'text': null, tooltip: "Battery power" }, + { 'name': "PWR_0", 'touch': 0, 'text': null, tooltip: "+3.3V" }, + { 'name': "PWR_1", 'touch': 0, 'text': null, tooltip: "+3.3V" }, + { 'name': "PWR_2", 'touch': 0, 'text': null, tooltip: "+3.3V" } + ]; + const MB_WIDTH = 169.82979; + const MB_HEIGHT = 259.11862; + export interface IBoardTheme { + accent?: string; + display?: string; + pin?: string; + pinTouched?: string; + pinActive?: string; + ledOn?: string; + ledOff?: string; + buttonOuter?: string; + buttonUps: string[]; + buttonDown?: string; + virtualButtonOuter?: string; + virtualButtonUp?: string; + virtualButtonDown?: string; + lightLevelOn?: string; + lightLevelOff?: string; + soundLevelOn?: string; + soundLevelOff?: string; + gestureButtonOn?: string; + gestureButtonOff?: string; + } + + export var themes: IBoardTheme[] = ["#3ADCFE"].map(accent => { + return { + accent: accent, + pin: "#D4AF37", + pinTouched: "#FFA500", + pinActive: "#FF5500", + ledOn: "#ff7777", + ledOff: "#fff", + buttonOuter: "#979797", + buttonUps: ["#FFF", "#4D4D4D", "#FFF", "#FFF", "#FFF", "#FFF", '#FFF'], + buttonDown: "#000", + virtualButtonDown: "#FFA500", + virtualButtonOuter: "#333", + virtualButtonUp: "#FFF", + lightLevelOn: "yellow", + lightLevelOff: "#555", + soundLevelOn: "#7f8c8d", + soundLevelOff: "#555", + gestureButtonOn: "#FFA500", + gestureButtonOff: "#B4009E" + } + }); + + export function randomTheme(): IBoardTheme { + return themes[Math.floor(Math.random() * themes.length)]; + } + + export interface IBoardProps { + runtime?: pxsim.Runtime; + theme?: IBoardTheme; + disableTilt?: boolean; + wireframe?: boolean; + } + + export class EV3BoardSvg implements BoardView { + public element: SVGSVGElement; + private style: SVGStyleElement; + private defs: SVGDefsElement; + private g: SVGGElement; + + private buttons: SVGElement[]; + private buttonABText: SVGTextElement; + private light: SVGElement; + private pins: SVGElement[]; + private pinControls: { [index: number]: AnalogPinControl }; + private systemLed: SVGCircleElement; + private irReceiver: SVGElement; + private irTransmitter: SVGElement; + private redLED: SVGRectElement; + private slideSwitch: SVGGElement; + private lightLevelButton: SVGCircleElement; + private lightLevelGradient: SVGLinearGradientElement; + private lightLevelText: SVGTextElement; + private soundLevelButton: SVGCircleElement; + private soundLevelGradient: SVGLinearGradientElement; + private soundLevelText: SVGTextElement; + private thermometerGradient: SVGLinearGradientElement; + private thermometer: SVGRectElement; + private thermometerText: SVGTextElement; + private antenna: SVGPolylineElement; + private shakeButtonGroup: SVGElement; + private shakeText: SVGTextElement; + public board: pxsim.DalBoard; + private pinNmToCoord: Map = { + }; + + constructor(public props: IBoardProps) { + this.buildDom(); + if (props && props.wireframe) + svg.addClass(this.element, "sim-wireframe"); + + /* + if (props && props.theme) + this.updateTheme(); + */ + if (props && props.runtime) { + this.board = this.props.runtime.board as pxsim.DalBoard; + this.board.updateSubscribers.push(() => this.updateState()); + this.updateState(); + this.attachEvents(); + } + } + + public getView(): SVGAndSize { + return { + el: this.element, + y: 0, + x: 0, + w: MB_WIDTH, + h: MB_HEIGHT + }; + } + + public getCoord(pinNm: string): Coord { + return this.pinNmToCoord[pinNm]; + } + + public highlightPin(pinNm: string): void { + //TODO: for instructions + } + + public getPinDist(): number { + return 10; + } + + private recordPinCoords() { + pinNames.forEach((pin, i) => { + const nm = pin.name; + const p = this.pins[i]; + const r = p.getBoundingClientRect(); + this.pinNmToCoord[nm] = [r.left + r.width / 2, r.top + r.height / 2]; + }); + console.log(JSON.stringify(this.pinNmToCoord, null, 2)) + } + + private updateTheme() { + let theme = this.props.theme; + + svg.fill(this.buttons[0], theme.buttonUps[0]); + svg.fill(this.buttons[1], theme.buttonUps[1]); + svg.fill(this.buttons[2], theme.buttonUps[2]); + + if (this.shakeButtonGroup) { + svg.fill(this.shakeButtonGroup, this.props.theme.gestureButtonOff); + } + + svg.setGradientColors(this.lightLevelGradient, theme.lightLevelOn, theme.lightLevelOff); + + svg.setGradientColors(this.thermometerGradient, theme.ledOff, theme.ledOn); + svg.setGradientColors(this.soundLevelGradient, theme.soundLevelOn, theme.soundLevelOff); + + for (const id in this.pinControls) { + this.pinControls[id].updateTheme(); + } + } + + public updateState() { + let state = this.board; + if (!state) return; + let theme = this.props.theme; + + let bpState = state.buttonState; + let buttons = bpState.buttons; + this.buttons.forEach((button, i) => { + svg.fill(button, buttons[i].pressed ? theme.buttonDown : theme.buttonUps[i]); + }) + + this.updateLight(); + /* + + this.updatePins(); + this.updateTilt(); + this.updateNeoPixels(); + this.updateSwitch(); + this.updateSound(); + this.updateLightLevel(); + this.updateSoundLevel(); + this.updateButtonAB(); + this.updateGestures(); + this.updateTemperature(); + this.updateInfrared(); + */ + + if (!runtime || runtime.dead) svg.addClass(this.element, "grayscale"); + else svg.removeClass(this.element, "grayscale"); + } + + private lastFlashTime: number = 0; + private flashSystemLed() { + /* + if (!this.systemLed) + this.systemLed = svg.child(this.g, "circle", { class: "sim-systemled", cx: 75, cy: MB_HEIGHT - 171, r: 2 }) + let now = Date.now(); + if (now - this.lastFlashTime > 150) { + this.lastFlashTime = now; + svg.animate(this.systemLed, "sim-flash") + } + */ + } + + private lastIrReceiverFlash: number = 0; + public flashIrReceiver() { + /* + if (!this.irReceiver) + this.irReceiver = this.element.getElementById("path2054") as SVGElement; + let now = Date.now(); + if (now - this.lastIrReceiverFlash > 200) { + this.lastIrReceiverFlash = now; + svg.animate(this.irReceiver, 'sim-flash-stroke') + } + */ + } + + private lastIrTransmitterFlash: number = 0; + public flashIrTransmitter() { + /* + if (!this.irTransmitter) + this.irTransmitter = this.element.getElementById("path2062") as SVGElement; + let now = Date.now(); + if (now - this.lastIrTransmitterFlash > 200) { + this.lastIrTransmitterFlash = now; + svg.animate(this.irTransmitter, 'sim-flash-stroke') + }*/ + } + + private updateInfrared() { + const state = this.board; + if (!state) return; + + if (state.irState.packetReceived) { + state.irState.packetReceived = false; + this.flashIrReceiver(); + } + } + + private lastLightPattern: number = -1; + private updateLight() { + let state = this.board; + if (!state || !state.lightState) return; + + const lightPattern = state.lightState.lightPattern; + if (lightPattern == this.lastLightPattern) return; + this.lastLightPattern = lightPattern; + switch(lightPattern) { + case 0: // LED_BLACK + svg.fill(this.light, "#FFF"); + break; + case 1: // LED_GREEN + svg.fill(this.light, "#00ff00"); + break; + case 2: // LED_RED + svg.fill(this.light, "#ff0000"); + break; + case 3: // LED_ORANGE + svg.fill(this.light, "#ffff00"); + break; + case 4: // LED_GREEN_FLASH + break; + case 5: // LED_RED_FLASH + break; + case 6: // LED_ORANGE_FLASH + break; + case 7: // LED_GREEN_PULSE + break; + case 8: // LED_RED_PULSE + break; + case 9: // LED_ORANGE_PULSE + break; + } + } + + private updateNeoPixels() { + let state = this.board; + if (!state || !state.neopixelState) return; + let neopixels = state.neopixelState.getNeoPixels(); + for (let i = 0; i < state.neopixelState.NUM_PIXELS; i++) { + let rgb = neopixels[i]; + let p_inner = this.element.getElementById(`LED${i}`) as SVGPathElement; + + if (!rgb || (rgb.length == 3 && rgb[0] == 0 && rgb[1] == 0 && rgb[2] == 0)) { + // Clear the pixel + svg.fill(p_inner, `rgb(200,200,200)`); + svg.filter(p_inner, null); + p_inner.style.stroke = `none` + continue; + } + + let hsl = visuals.rgbToHsl(rgb); + let [h, s, l] = hsl; + let lx = Math.max(l * 1.3, 85); + // at least 10% luminosity + l = l * 90 / 100 + 10; + if (p_inner) { + p_inner.style.stroke = `hsl(${h}, ${s}%, ${Math.min(l * 3, 75)}%)` + p_inner.style.strokeWidth = "1.5"; + svg.fill(p_inner, `hsl(${h}, ${s}%, ${lx}%)`) + } + if (p_inner) svg.filter(p_inner, `url(#neopixelglow)`); + } + } + + private updateSound() { + let state = this.board; + if (!state || !state.audioState) return; + let audioState = state.audioState; + + // FIXME + // let soundBoard = this.element.getElementById('g4656') as SVGGElement; + // if (audioState.isPlaying()) { + // svg.addClass(soundBoard, "sim-sound-stroke"); + // } else { + // svg.removeClass(soundBoard, "sim-sound-stroke"); + // } + } + + private updatePins() { + let state = this.board; + if (!state || !state.edgeConnectorState || !state.capacitiveSensorState) return; + state.edgeConnectorState.pins.forEach((pin, i) => this.updatePin(pin, i)); + } + + private updatePin(pin: Pin, index: number) { + if (!pin || !this.pins[index]) return; + + if ((pin as pins.CommonPin).used) { + if (this.pinControls[pin.id] === undefined) { + const pinName = pinNames.filter((a) => a.id === pin.id)[0]; + if (pinName) { + this.pinControls[pin.id] = new AnalogPinControl(this, this.defs, pin.id, pinName.name); + } + else { + // TODO: Surface pin controls for sensor pins in some way? + this.pinControls[pin.id] = null; + } + } + + if (this.pinControls[pin.id]) { + this.pinControls[pin.id].updateValue(); + } + } + } + + private updateLightLevel() { + let state = this.board; + if (!state || !state.lightSensorState.sensorUsed) return; + + if (!this.lightLevelButton) { + let gid = "gradient-light-level"; + this.lightLevelGradient = svg.linearGradient(this.defs, gid) + let cy = 15; + let r = 10; + this.lightLevelButton = svg.child(this.g, "circle", { + cx: `12px`, cy: `${cy}px`, r: `${r}px`, + class: 'sim-light-level-button', + fill: `url(#${gid})` + }) as SVGCircleElement; + let pt = this.element.createSVGPoint(); + svg.buttonEvents(this.lightLevelButton, + (ev) => { + let pos = svg.cursorPoint(pt, this.element, ev); + let rs = r / 2; + let level = Math.max(0, Math.min(255, Math.floor((pos.y - (cy - rs)) / (2 * rs) * 255))); + if (level != this.board.lightSensorState.getLevel()) { + this.board.lightSensorState.setLevel(level); + this.applyLightLevel(); + } + }, ev => { }, + ev => { }) + this.lightLevelText = svg.child(this.g, "text", { x: 23, y: cy + r - 15, text: '', class: 'sim-text' }) as SVGTextElement; + this.updateTheme(); + } + + svg.setGradientValue(this.lightLevelGradient, Math.min(100, Math.max(0, Math.floor(state.lightSensorState.getLevel() * 100 / 255))) + '%') + this.lightLevelText.textContent = state.lightSensorState.getLevel().toString(); + } + + private applyLightLevel() { + let lv = this.board.lightSensorState.getLevel(); + svg.setGradientValue(this.lightLevelGradient, Math.min(100, Math.max(0, Math.floor(lv * 100 / 255))) + '%') + this.lightLevelText.textContent = lv.toString(); + } + + private updateSoundLevel() { + let state = this.board; + if (!state || !state.microphoneState.sensorUsed) return; + + if (!this.soundLevelButton) { + let gid = "gradient-sound-level"; + this.soundLevelGradient = svg.linearGradient(this.defs, gid) + let cy = 165; + let r = 10; + this.soundLevelButton = svg.child(this.g, "circle", { + cx: `12px`, cy: `${cy}px`, r: `${r}px`, + class: 'sim-sound-level-button', + fill: `url(#${gid})` + }) as SVGCircleElement; + + let pt = this.element.createSVGPoint(); + svg.buttonEvents(this.soundLevelButton, + (ev) => { + let pos = svg.cursorPoint(pt, this.element, ev); + let rs = r / 2; + let level = Math.max(0, Math.min(255, Math.floor((pos.y - (cy - rs)) / (2 * rs) * 255))); + if (level != this.board.microphoneState.getLevel()) { + this.board.microphoneState.setLevel(255 - level); + this.applySoundLevel(); + } + }, ev => { }, + ev => { }) + this.soundLevelText = svg.child(this.g, "text", { x: 23, y: cy + r - 3, text: '', class: 'sim-text' }) as SVGTextElement; + this.updateTheme(); + } + + svg.setGradientValue(this.soundLevelGradient, Math.min(100, Math.max(0, Math.floor((255 - state.microphoneState.getLevel()) * 100 / 255))) + '%') + this.soundLevelText.textContent = state.microphoneState.getLevel().toString(); + } + + private applySoundLevel() { + let lv = this.board.microphoneState.getLevel(); + svg.setGradientValue(this.soundLevelGradient, Math.min(100, Math.max(0, Math.floor((255 - lv) * 100 / 255))) + '%') + this.soundLevelText.textContent = lv.toString(); + } + + private updateTemperature() { + let state = this.board; + if (!state || !state.thermometerState || !state.thermometerState.sensorUsed) return; + + // Celsius + let tmin = -5; + let tmax = 50; + if (!this.thermometer) { + let gid = "gradient-thermometer"; + this.thermometerGradient = svg.linearGradient(this.defs, gid); + this.thermometer = svg.child(this.g, "rect", { + class: "sim-thermometer", + x: 170, + y: 3, + width: 7, + height: 32, + rx: 2, ry: 2, + fill: `url(#${gid})` + }); + this.thermometerText = svg.child(this.g, "text", { class: 'sim-text', x: 148, y: 10 }) as SVGTextElement; + this.updateTheme(); + + let pt = this.element.createSVGPoint(); + svg.buttonEvents(this.thermometer, + (ev) => { + let cur = svg.cursorPoint(pt, this.element, ev); + let t = Math.max(0, Math.min(1, (35 - cur.y) / 30)) + state.thermometerState.setLevel(Math.floor(tmin + t * (tmax - tmin))); + this.updateTemperature(); + }, ev => { }, ev => { }) + } + + let t = Math.max(tmin, Math.min(tmax, state.thermometerState.getLevel())) + let per = Math.floor((state.thermometerState.getLevel() - tmin) / (tmax - tmin) * 100) + svg.setGradientValue(this.thermometerGradient, 100 - per + "%"); + + let unit = "°C"; + if (state.thermometerUnitState == pxsim.TemperatureUnit.Fahrenheit) { + unit = "°F"; + t = ((t * 18) / 10 + 32) >> 0; + } + this.thermometerText.textContent = t + unit; + } + + private updateButtonAB() { + let state = this.board; + if (state.buttonState.usesButtonAB) { + (this.buttons[2]).style.visibility = "visible"; + this.updateTheme(); + } + } + + private updateGestures() { + let state = this.board; + if (state.accelerometerState.useShake && !this.shakeButtonGroup) { + const btnr = 2; + const width = 22; + const height = 10; + + let btng = svg.child(this.g, "g", { class: "sim-button-group" }); + this.shakeButtonGroup = btng; + this.shakeText = svg.child(this.g, "text", { x: 81, y: 32, class: "sim-text small" }) as SVGTextElement; + this.shakeText.textContent = "SHAKE" + + svg.child(btng, "rect", { class: "sim-button", x: 79, y: 25, rx: btnr, ry: btnr, width, height }); + svg.fill(btng, this.props.theme.gestureButtonOff); + this.shakeButtonGroup.addEventListener(pointerEvents.down, ev => { + let state = this.board; + svg.fill(btng, this.props.theme.gestureButtonOn); + svg.addClass(this.shakeText, "inverted"); + }) + this.shakeButtonGroup.addEventListener(pointerEvents.leave, ev => { + let state = this.board; + svg.fill(btng, this.props.theme.gestureButtonOff); + svg.removeClass(this.shakeText, "inverted"); + }) + this.shakeButtonGroup.addEventListener(pointerEvents.up, ev => { + let state = this.board; + svg.fill(btng, this.props.theme.gestureButtonOff); + //this.board.bus.queue(DAL.DEVICE_ID_GESTURE, 11); // GESTURE_SHAKE + svg.removeClass(this.shakeText, "inverted"); + }) + } + } + + private updateTilt() { + if (this.props.disableTilt) return; + let state = this.board; + if (!state || !state.accelerometerState.accelerometer.isActive) return; + + const x = state.accelerometerState.accelerometer.getX(); + const y = state.accelerometerState.accelerometer.getY(); + const af = 8 / 1023; + const s = 1 - Math.min(0.1, Math.pow(Math.max(Math.abs(x), Math.abs(y)) / 1023, 2) / 35); + + this.element.style.transform = `perspective(30em) rotateX(${y * af}deg) rotateY(${x * af}deg) scale(${s}, ${s})` + this.element.style.perspectiveOrigin = "50% 50% 50%"; + this.element.style.perspective = "30em"; + } + + private buildDom() { + this.element = new DOMParser().parseFromString(BOARD_SVG, "image/svg+xml").querySelector("svg") as SVGSVGElement; + svg.hydrate(this.element, { + "version": "1.0", + "viewBox": `0 0 ${MB_WIDTH} ${MB_HEIGHT}`, + "class": "sim", + "x": "0px", + "y": "0px", + "width": MB_WIDTH + "px", + "height": MB_HEIGHT + "px", + }); + this.style = svg.child(this.element, "style", {}); + this.style.textContent = MB_STYLE; + + this.defs = svg.child(this.element, "defs", {}); + this.g = svg.elt("g"); + this.element.appendChild(this.g); + + const btnids = ["BTN_1", "BTN_2", "BTN_3", "BTN_4", "BTN_5", "BTN_BACK"]; + this.buttons = btnids.map(n => this.element.getElementById(n) as SVGElement); + this.buttons.forEach(b => svg.addClass(b, "sim-button")); + + this.light = this.element.getElementById("BOARD_Light") as SVGElement; + } + + private attachEvents() { + Runtime.messagePosted = (msg) => { + switch (msg.type || "") { + case "serial": this.flashSystemLed(); break; + case "irpacket": this.flashIrTransmitter(); break; + } + } + + /* + let tiltDecayer = 0; + this.element.addEventListener(pointerEvents.move, (ev: MouseEvent) => { + let state = this.board; + if (!state.accelerometerState.accelerometer.isActive) return; + + if (tiltDecayer) { + clearInterval(tiltDecayer); + tiltDecayer = 0; + } + + let bbox = this.element.getBoundingClientRect(); + let ax = (ev.clientX - bbox.width / 2) / (bbox.width / 3); + let ay = (ev.clientY - bbox.height / 2) / (bbox.height / 3); + + let x = - Math.max(- 1023, Math.min(1023, Math.floor(ax * 1023))); + let y = Math.max(- 1023, Math.min(1023, Math.floor(ay * 1023))); + let z2 = 1023 * 1023 - x * x - y * y; + let z = Math.floor((z2 > 0 ? -1 : 1) * Math.sqrt(Math.abs(z2))); + + state.accelerometerState.accelerometer.update(x, y, z); + this.updateTilt(); + }, false); + this.element.addEventListener(pointerEvents.leave, (ev: MouseEvent) => { + let state = this.board; + if (!state.accelerometerState.accelerometer.isActive) return; + + if (!tiltDecayer) { + tiltDecayer = setInterval(() => { + let accx = state.accelerometerState.accelerometer.getX(MicroBitCoordinateSystem.RAW); + accx = Math.floor(Math.abs(accx) * 0.85) * (accx > 0 ? 1 : -1); + let accy = state.accelerometerState.accelerometer.getY(MicroBitCoordinateSystem.RAW); + accy = Math.floor(Math.abs(accy) * 0.85) * (accy > 0 ? 1 : -1); + let accz = -Math.sqrt(Math.max(0, 1023 * 1023 - accx * accx - accy * accy)); + if (Math.abs(accx) <= 24 && Math.abs(accy) <= 24) { + clearInterval(tiltDecayer); + tiltDecayer = 0; + accx = 0; + accy = 0; + accz = -1023; + } + state.accelerometerState.accelerometer.update(accx, accy, accz); + this.updateTilt(); + }, 50) + } + }, false); + */ + let bpState = this.board.buttonState; + let stateButtons = bpState.buttons; + this.buttons.forEach((btn, index) => { + let button = stateButtons[index]; + + btn.addEventListener(pointerEvents.down, ev => { + button.setPressed(true); + svg.fill(this.buttons[index], this.props.theme.buttonDown); + }) + btn.addEventListener(pointerEvents.leave, ev => { + button.setPressed(false); + svg.fill(this.buttons[index], this.props.theme.buttonUps[index]); + }) + btn.addEventListener(pointerEvents.up, ev => { + button.setPressed(false); + svg.fill(this.buttons[index], this.props.theme.buttonUps[index]); + }) + }) + } + } +} \ No newline at end of file diff --git a/sim/visuals/boardsvg.ts b/sim/visuals/boardsvg.ts new file mode 100644 index 00000000..e25f69a9 --- /dev/null +++ b/sim/visuals/boardsvg.ts @@ -0,0 +1,1178 @@ +namespace pxsim.visuals { + export const BOARD_SVG = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; +} \ No newline at end of file diff --git a/sim/visuals/boardview.ts b/sim/visuals/boardview.ts new file mode 100644 index 00000000..aa79f666 --- /dev/null +++ b/sim/visuals/boardview.ts @@ -0,0 +1,10 @@ +namespace pxsim.visuals { + mkBoardView = (opts: BoardViewOptions): BoardView => { + return new visuals.EV3BoardSvg({ + runtime: runtime, + theme: visuals.randomTheme(), + disableTilt: false, + wireframe: opts.wireframe, + }); + } +} \ No newline at end of file diff --git a/sim/visuals/pincontrol.ts b/sim/visuals/pincontrol.ts new file mode 100644 index 00000000..c3e729b0 --- /dev/null +++ b/sim/visuals/pincontrol.ts @@ -0,0 +1,92 @@ +namespace pxsim.visuals { + export class AnalogPinControl { + private outerElement: SVGElement; + + private innerCircle: SVGCircleElement; + private gradient: SVGLinearGradientElement; + private currentValue: number; + private pin: Pin; + + constructor(private parent: EV3BoardSvg, private defs: SVGDefsElement, private id: CPlayPinName, name: string) { + this.pin = board().edgeConnectorState.getPin(this.id); + + // Init the button events + this.outerElement = parent.element.getElementById(name) as SVGElement; + svg.addClass(this.outerElement, "sim-pin-touch"); + this.addButtonEvents(); + + + // Init the gradient controls + // const gid = `gradient-${CPlayPinName[id]}-level`; + // this.innerCircle = parent.element.getElementById("PIN_CONNECTOR_" + CPlayPinName[id]) as SVGCircleElement; + // this.gradient = svg.linearGradient(this.defs, gid); + // this.innerCircle.setAttribute("fill", `url(#${gid})`); + // this.innerCircle.setAttribute("class", "sim-light-level-button") + // this.addLevelControlEvents() + + this.updateTheme(); + } + + public updateTheme() { + const theme = this.parent.props.theme; + svg.setGradientColors(this.gradient, theme.lightLevelOff, 'darkorange'); + } + + public updateValue() { + const value = this.pin.value; + + if (value === this.currentValue) { + return; + } + + this.currentValue = value; + + // svg.setGradientValue(this.gradient, 100 - Math.min(100, Math.max(0, Math.floor(value * 100 / 1023))) + '%') + // if (this.innerCircle.childNodes.length) { + // this.innerCircle.removeChild(this.innerCircle.childNodes[0]) + // } + + svg.title(this.outerElement, value.toString()); + } + + private addButtonEvents() { + this.outerElement.addEventListener(pointerEvents.down, ev => { + this.pin.touched = true; + svg.addClass(this.outerElement, "touched"); + + (pxtcore.getTouchButton(this.id - 1) as CommonButton).setPressed(true); + }) + this.outerElement.addEventListener(pointerEvents.leave, ev => { + this.pin.touched = false; + svg.removeClass(this.outerElement, "touched"); + + (pxtcore.getTouchButton(this.id - 1) as CommonButton).setPressed(false); + }) + this.outerElement.addEventListener(pointerEvents.up, ev => { + this.pin.touched = false; + svg.removeClass(this.outerElement, "touched"); + + (pxtcore.getTouchButton(this.id - 1) as CommonButton).setPressed(false); + }) + } + + private addLevelControlEvents() { + const cy = parseFloat(this.innerCircle.getAttribute("cy")); + const r = parseFloat(this.innerCircle.getAttribute("r")); + const pt = this.parent.element.createSVGPoint(); + + svg.buttonEvents(this.innerCircle, + (ev) => { + const pos = svg.cursorPoint(pt, this.parent.element, ev); + const rs = r / 2; + const level = Math.max(0, Math.min(1023, Math.floor((1 - (pos.y - (cy - rs)) / (2 * rs)) * 1023))); + + if (level != this.pin.value) { + this.pin.value = level; + this.updateValue(); + } + }, ev => { }, + ev => { }); + } + } +} \ No newline at end of file diff --git a/theme/site/globals/site.variables b/theme/site/globals/site.variables index f25365bc..3d4640b7 100644 --- a/theme/site/globals/site.variables +++ b/theme/site/globals/site.variables @@ -121,7 +121,7 @@ Full screen --------------------*/ -@fullscreenBackgroundGradientStart: @mainMenuInvertedBackground; +@fullscreenBackgroundGradientStart: @blue; @fullscreenBackgroundGradientEnd: #fff; /*------------------- diff --git a/theme/style.less b/theme/style.less index 766ea0b9..58cc9c5e 100644 --- a/theme/style.less +++ b/theme/style.less @@ -46,6 +46,10 @@ #menubar .ui.item { color: @red; } +.fullscreensim #menubar .ui.menu { + box-shadow: none !important; + border-bottom: 0 !important; +} /* logo */ #logo a.ui.image .ui.logo {