From 1cc69ba4a11769cb4e534e0e62a901f2e74640f6 Mon Sep 17 00:00:00 2001 From: Michal Moskal Date: Thu, 10 Mar 2016 16:24:11 -0800 Subject: [PATCH] Add simulator --- package.json | 3 +- sim/enums.ts | 81 ++++++ sim/libmbit.ts | 381 ++++++++++++++++++++++++++++ sim/public/sim.css | 74 ++++++ sim/public/simulator.html | 20 ++ sim/simsvg.ts | 509 ++++++++++++++++++++++++++++++++++++++ sim/state.ts | 348 ++++++++++++++++++++++++++ sim/tsconfig.json | 12 + 8 files changed, 1427 insertions(+), 1 deletion(-) create mode 100644 sim/enums.ts create mode 100644 sim/libmbit.ts create mode 100644 sim/public/sim.css create mode 100644 sim/public/simulator.html create mode 100644 sim/simsvg.ts create mode 100644 sim/state.ts create mode 100644 sim/tsconfig.json diff --git a/package.json b/package.json index b90b5cfe..ec0cb7d6 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "README.md" ], "dependencies": { - "kindscript": "*" + "kindscript": "*", + "typescript": "^1.8.7" } } diff --git a/sim/enums.ts b/sim/enums.ts new file mode 100644 index 00000000..a83c547f --- /dev/null +++ b/sim/enums.ts @@ -0,0 +1,81 @@ +namespace ks.rt.micro_bit { + export interface Enums { + MES_ALERT_EVT_ALARM1: number; + MES_ALERT_EVT_ALARM2: number; + MES_ALERT_EVT_ALARM3: number; + MES_ALERT_EVT_ALARM4: number; + MES_ALERT_EVT_ALARM5: number; + MES_ALERT_EVT_ALARM6: number; + MES_ALERT_EVT_DISPLAY_TOAST: number; + MES_ALERT_EVT_FIND_MY_PHONE: number; + MES_ALERT_EVT_PLAY_RINGTONE: number; + MES_ALERT_EVT_PLAY_SOUND: number; + MES_ALERT_EVT_VIBRATE: number; + MES_CAMERA_EVT_LAUNCH_PHOTO_MODE: number; + MES_CAMERA_EVT_LAUNCH_VIDEO_MODE: number; + MES_CAMERA_EVT_START_VIDEO_CAPTURE: number; + MES_CAMERA_EVT_STOP_PHOTO_MODE: number; + MES_CAMERA_EVT_STOP_VIDEO_CAPTURE: number; + MES_CAMERA_EVT_STOP_VIDEO_MODE: number; + MES_CAMERA_EVT_TAKE_PHOTO: number; + MES_CAMERA_EVT_TOGGLE_FRONT_REAR: number; + MES_DEVICE_DISPLAY_OFF: number; + MES_DEVICE_DISPLAY_ON: number; + MES_DEVICE_GESTURE_DEVICE_SHAKEN: number; + MES_DEVICE_INCOMING_CALL: number; + MES_DEVICE_INCOMING_MESSAGE: number; + MES_DEVICE_ORIENTATION_LANDSCAPE: number; + MES_DEVICE_ORIENTATION_PORTRAIT: number; + MES_DPAD_BUTTON_1_DOWN: number; + MES_DPAD_BUTTON_1_UP: number; + MES_DPAD_BUTTON_2_DOWN: number; + MES_DPAD_BUTTON_2_UP: number; + MES_DPAD_BUTTON_3_DOWN: number; + MES_DPAD_BUTTON_3_UP: number; + MES_DPAD_BUTTON_4_DOWN: number; + MES_DPAD_BUTTON_4_UP: number; + MES_DPAD_BUTTON_A_DOWN: number; + MES_DPAD_BUTTON_A_UP: number; + MES_DPAD_BUTTON_B_DOWN: number; + MES_DPAD_BUTTON_B_UP: number; + MES_DPAD_BUTTON_C_DOWN: number; + MES_DPAD_BUTTON_C_UP: number; + MES_DPAD_BUTTON_D_DOWN: number; + MES_DPAD_BUTTON_D_UP: number; + MES_REMOTE_CONTROL_EVT_FORWARD: number; + MES_REMOTE_CONTROL_EVT_NEXTTRACK: number; + MES_REMOTE_CONTROL_EVT_PAUSE: number; + MES_REMOTE_CONTROL_EVT_PLAY: number; + MES_REMOTE_CONTROL_EVT_PREVTRACK: number; + MES_REMOTE_CONTROL_EVT_REWIND: number; + MES_REMOTE_CONTROL_EVT_STOP: number; + MES_REMOTE_CONTROL_EVT_VOLUMEDOWN: number; + MES_REMOTE_CONTROL_EVT_VOLUMEUP: number; + MICROBIT_ID_BUTTON_A: number; + MICROBIT_ID_BUTTON_B: number; + MICROBIT_ID_BUTTON_AB: number; + MICROBIT_BUTTON_EVT_CLICK: number; + MICROBIT_ID_IO_P0: number; + MICROBIT_ID_IO_P1: number; + MICROBIT_ID_IO_P2: number; + MICROBIT_ID_IO_P3: number; + MICROBIT_ID_IO_P4: number; + MICROBIT_ID_IO_P5: number; + MICROBIT_ID_IO_P6: number; + MICROBIT_ID_IO_P7: number; + MICROBIT_ID_IO_P8: number; + MICROBIT_ID_IO_P9: number; + MICROBIT_ID_IO_P10: number; + MICROBIT_ID_IO_P11: number; + MICROBIT_ID_IO_P12: number; + MICROBIT_ID_IO_P13: number; + MICROBIT_ID_IO_P14: number; + MICROBIT_ID_IO_P15: number; + MICROBIT_ID_IO_P16: number; + MICROBIT_ID_IO_P19: number; + MICROBIT_ID_IO_P20: number; + MES_BROADCAST_GENERAL_ID: number; + MICROBIT_ID_RADIO: number; + MICROBIT_RADIO_EVT_DATAGRAM: number; + } +} \ No newline at end of file diff --git a/sim/libmbit.ts b/sim/libmbit.ts new file mode 100644 index 00000000..417e45c5 --- /dev/null +++ b/sim/libmbit.ts @@ -0,0 +1,381 @@ +/// +/// + +namespace ks.rt.micro_bit { + export function initCurrentRuntime() { + initBoard(); + } + + ks.rt.initCurrentRuntime = initCurrentRuntime; + + function initBoard() { + U.assert(!runtime.board) + runtime.board = new Board() + } + + export function board() { + return runtime.board as Board + } + + export function enums() { + return runtime.enums as any as Enums + } + + export interface AnimationOptions { + interval: number; + // false means last frame + frame: () => boolean; + whenDone?: (cancelled: boolean) => void; + } + + export class AnimationQueue { + private queue: AnimationOptions[] = []; + private process: () => void; + + constructor(private runtime: Runtime) { + this.process = () => { + let top = this.queue[0] + if (!top) return + if (this.runtime.dead) return + runtime = this.runtime + let res = top.frame() + runtime.queueDisplayUpdate() + runtime.maybeUpdateDisplay() + if (res === false) { + this.queue.shift(); + // if there is already something in the queue, start processing + if (this.queue[0]) + setTimeout(this.process, this.queue[0].interval) + // this may push additional stuff + top.whenDone(false); + } else { + setTimeout(this.process, top.interval) + } + } + } + + public cancelAll() { + let q = this.queue + this.queue = [] + for (let a of q) { + a.whenDone(true) + } + } + + public cancelCurrent() { + let top = this.queue[0] + if (top) { + this.queue.shift(); + top.whenDone(true); + } + } + + public enqueue(anim: AnimationOptions) { + if (!anim.whenDone) anim.whenDone = () => { }; + this.queue.push(anim) + // we start processing when the queue goes from 0 to 1 + if (this.queue.length == 1) + this.process() + } + + public executeAsync(anim: AnimationOptions) { + U.assert(!anim.whenDone) + return new Promise((resolve, reject) => { + anim.whenDone = resolve + this.enqueue(anim) + }) + } + } + + export function panic(code: number) { + console.log("PANIC:", code) + throw new Error("PANIC " + code) + } + + /* basic */ + export function showDigit(v: number) { + if (!quiet) + console.log("DIGIT:", v) + } + + export function clearScreen() { + board().image.clear(); + runtime.queueDisplayUpdate() + } + + export function showLeds(leds: micro_bit.Image, delay: number): void { + showAnimation(leds, delay); + } + + function scrollImage(leds: micro_bit.Image, interval: number, stride: number): void { + let cb = getResume() + let off = stride > 0 ? 0 : leds.width - 1; + + board().animationQ.enqueue({ + interval: interval, + frame: () => { + if (off >= leds.width || off < 0) + return false; + let c = Math.min(5, leds.width - off); + leds.copyTo(off, 5, board().image, 0) + off += stride; + return true; + }, + whenDone: cb + }) + } + + export function showAnimation(leds: micro_bit.Image, interval: number = 400): void { + scrollImage(leds, interval, 5); + } + + export function scrollNumber(x: number, interval: number) { + if (interval < 0) return; + + let leds = createImageFromString(x.toString()); + if (x < 0 || x >= 10) scrollImage(leds, interval, 5); + else showLeds(leds, interval * 5); + } + + export function scrollString(s: string, interval: number) { + if (interval < 0) return; + if (s.length == 0) { + clearScreen(); + pause(interval * 5); + } else { + let leds = createImageFromString(s); + if (s.length == 1) showLeds(leds, interval * 5) + else scrollImage(leds, interval, 1); + } + } + + export function forever(a: RefAction) { + function loop() { + runtime.runFiberAsync(a) + .then(() => Promise.delay(20)) + .then(loop) + .done() + } + incr(a) + loop() + } + + export var pause = thread.pause; + + /* leds */ + export function plot(x: number, y: number) { + board().image.set(x, y, 255); + runtime.queueDisplayUpdate() + } + + export function unPlot(x: number, y: number) { + board().image.set(x, y, 0); + runtime.queueDisplayUpdate() + } + + export function point(x: number, y: number): boolean { + return !!board().image.get(x, y); + } + + export function brightness(): number { + return board().brigthness; + } + + export function setBrightness(value: number): void { + board().brigthness = value; + runtime.queueDisplayUpdate() + } + + export function stopAnimation(): void { + board().animationQ.cancelAll(); + } + + export function plotLeds(leds: micro_bit.Image): void { + leds.copyTo(0, 5, board().image, 0) + runtime.queueDisplayUpdate() + } + + export function setDisplayMode(mode: DisplayMode): void { + board().displayMode = mode; + runtime.queueDisplayUpdate() + } + + /* control */ + export var runInBackground = thread.runInBackground; + + /* serial */ + export function serialSendString(s: string) { + board().writeSerial(s); + } + + export function serialReadString() : string { + return board().readSerial(); + } + + /* input */ + export function onButtonPressed(button : number, handler: RefAction) : void { + let ens = enums(); + board().bus.listen(button, ens.MICROBIT_BUTTON_EVT_CLICK, handler); + } + + export function isButtonPressed(button: number): boolean { + var ens = enums(); + if (button == ens.MICROBIT_ID_BUTTON_AB && !board().usesButtonAB) { + board().usesButtonAB = true; + runtime.queueDisplayUpdate(); + } + var bts = board().buttons; + if (button == ens.MICROBIT_ID_BUTTON_A) return bts[0].pressed; + if (button == ens.MICROBIT_ID_BUTTON_B) return bts[1].pressed; + return bts[2].pressed || (bts[0].pressed && bts[1].pressed); + } + + export function onPinPressed(pin: Pin, handler: RefAction) { + pin.isTouched(); + onButtonPressed(pin.id, handler); + } + + export function ioP0() { return board().pins[0]; } + export function ioP1() { return board().pins[1]; } + export function ioP2() { return board().pins[2]; } + export function ioP3() { return board().pins[3]; } + export function ioP4() { return board().pins[4]; } + export function ioP5() { return board().pins[5]; } + export function ioP6() { return board().pins[6]; } + export function ioP7() { return board().pins[7]; } + export function ioP8() { return board().pins[8]; } + export function ioP9() { return board().pins[9]; } + export function ioP10() { return board().pins[10]; } + export function ioP11() { return board().pins[11]; } + export function ioP12() { return board().pins[12]; } + export function ioP13() { return board().pins[13]; } + export function ioP14() { return board().pins[14]; } + export function ioP15() { return board().pins[15]; } + export function ioP16() { return board().pins[16]; } + export function ioP19() { return board().pins[19]; } + export function ioP20() { return board().pins[20]; } + + export function isPinTouched(pin: Pin): boolean { + return pin.isTouched(); + } + + export function compassHeading(): number { + var b = board(); + if (!b.usesHeading) { + b.usesHeading = true; + runtime.queueDisplayUpdate(); + } + return b.heading; + } + + export function getAcceleration(dimension: number): number { + var b = board(); + if (!b.usesAcceleration) { + b.usesAcceleration = true; + runtime.queueDisplayUpdate(); + } + var acc = b.acceleration; + switch (dimension) { + case 0: return acc[0]; + case 1: return acc[1]; + case 2: return acc[2]; + default: return Math.sqrt(acc[0] * acc[0] + acc[1] * acc[1] + acc[2] * acc[2]); + } + } + + export function lightLevel(): number { + // TODO + return 0; + } + + export function getMagneticForce(): number { + // TODO + return 0; + } + + export function getCurrentTime(): number { + return runtime.runningTime(); + } + + /* pins */ + export function digitalReadPin(pin : Pin) : number { + pin.mode = PinMode.Digital | PinMode.Input; + return pin.value > 100 ? 1 : 0; + } + + export function digitalWritePin(pin : Pin, value: number) { + pin.mode = PinMode.Digital | PinMode.Output; + pin.value = value > 0 ? 1023 : 0; + board().updateView(); + } + + export function analogReadPin(pin : Pin) : number { + pin.mode = PinMode.Analog | PinMode.Input; + return pin.value || 0; + } + + export function analogWritePin(pin : Pin, value: number) { + pin.mode = PinMode.Analog | PinMode.Output; + pin.value = value ? 1 : 0; + board().updateView(); + } + + export function setAnalogPeriodUs(pin: Pin, micros:number) { + pin.mode = PinMode.Analog | PinMode.Output; + } + + export function servoWritePin(pin: Pin, value: number) { + setAnalogPeriodUs(pin, 20000); + // TODO + } + + export function servoSetPulse(pin: Pin, micros:number) { + } + + export function enablePitch(pin: Pin) { + pin.mode = PinMode.Analog | PinMode.Output; + } + + export function pitch(frequency: number, ms: number) { + let cb = getResume(); + setTimeout(() => { cb() }, ms); + } + + + /* radio */ + export function broadcastMessage(msg: number) : void { + board().radio.broadcast(msg); + } + + export function onBroadcastMessageReceived(msg: number, handler: RefAction) : void { + let ens = enums() + board().bus.listen(ens.MES_BROADCAST_GENERAL_ID, msg, handler); + } + + export function setGroup(id : number) : void { + board().radio.setGroup(id); + } + + + export function datagramSendNumbers(value0 : number, value1: number, value2: number, value3: number) : void { + board().radio.datagram.send([value0, value1, value2, value3]); + } + + export function datagramReceiveNumber() : number { + return board().radio.datagram.recv().data[0]; + } + + export function datagramGetNumber(index : number) : number { + return board().radio.datagram.lastReceived.data[index] || 0; + } + + export function datagramGetRSSI() : number { + return board().radio.datagram.lastReceived.rssi; + } + + export function onDatagramReceived(handler: RefAction) : void { + let ens = enums(); + board().bus.listen(ens.MICROBIT_ID_RADIO, ens.MICROBIT_RADIO_EVT_DATAGRAM, handler); + } +} + diff --git a/sim/public/sim.css b/sim/public/sim.css new file mode 100644 index 00000000..7603416e --- /dev/null +++ b/sim/public/sim.css @@ -0,0 +1,74 @@ +svg.sim { + margin-bottom:1em; +} +.sim-button { + pointer-events: none; +} + +.sim-button-outer:hover { + stroke:grey; + stroke-width: 3px; +} + +.sim-pin-touch:hover { + stroke:#D4AF37; + stroke-width:2px; +} + +.sim-pin-touch.touched:hover { + stroke:darkorange; +} + +.sim-led-back:hover { + stroke:#a0a0a0; + stroke-width:3px; +} +.sim-led:hover { + stroke:#ff7f7f; + stroke-width:3px; +} + +.sim-systemled { + fill:#333; + stroke:#555; + stroke-width: 1px; +} + +.sim-antenna { + stroke:#555; + stroke-width: 2px; +} + +.sim-text { + font-family:monospace; + font-size:25px; + fill:#fff; +} + +.sim-text-pin { + font-family:monospace; + font-size:20px; + fill:#fff; +} + +/* animations */ +.sim-flash { + animation-name: sim-flash-animation; + animation-duration: 0.1s; +} + +@keyframes sim-flash-animation { + from { fill: yellow; } + to { fill: default; } +} + +.sim-flash-stroke { + 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; } +} \ No newline at end of file diff --git a/sim/public/simulator.html b/sim/public/simulator.html new file mode 100644 index 00000000..52ea14d2 --- /dev/null +++ b/sim/public/simulator.html @@ -0,0 +1,20 @@ + + + + + + microbit simulator + + + + + + + + + diff --git a/sim/simsvg.ts b/sim/simsvg.ts new file mode 100644 index 00000000..c2391a4d --- /dev/null +++ b/sim/simsvg.ts @@ -0,0 +1,509 @@ +namespace ks.rt.micro_bit { + + export interface IBoardTheme { + accent?: string; + display?: string; + pin?: string; + pinTouched?: string; + pinActive?: string; + ledOn?: string; + ledOff?: string; + buttonOuter?: string; + buttonUp?: string; + buttonDown?: string; + } + + export var themes: IBoardTheme[] = ["#3ADCFE", "#FFD43A", "#3AFFB3", "#FF3A54"].map(accent => { + return { + accent: accent, + display: "#000", + pin: "#D4AF37", + pinTouched: "#FFA500", + pinActive:"#FF5500", + ledOn: "#ff7f7f", + ledOff: "#202020", + buttonOuter: "#979797", + buttonUp: "#000", + buttonDown: "#FFA500", + }}); + + export function randomTheme() : IBoardTheme { + return themes[Math.floor(Math.random() * themes.length)]; + } + + export interface IBoardProps { + runtime: ks.rt.Runtime; + theme?: IBoardTheme; + disableTilt?:boolean; + } + + class Svg { + static pt : SVGPoint; + static cursorPoint(pt: SVGPoint, svg: SVGSVGElement, evt : MouseEvent) : SVGPoint { + pt.x = evt.clientX; + pt.y = evt.clientY; + return pt.matrixTransform(svg.getScreenCTM().inverse()); + } + + static rotateElement(el : SVGElement, originX : number ,originY : number,degrees:number){ + el.setAttribute( + 'transform', + `translate(${originX},${originY}) rotate(${degrees+90}) translate(${-originX},${-originY})` + ); + } + + + static elt(name:string) : SVGElement { + return document.createElementNS("http://www.w3.org/2000/svg", name) + } + + static hydrate(el : SVGElement, props: any) { + Object.keys(props).forEach(k => el.setAttributeNS(null, k, props[k])); + } + + static child(parent : Element, name: string, props: any) : SVGElement { + var el = Svg.elt(name); + Svg.hydrate(el, props); + parent.appendChild(el); + return el; + } + + static path(parent: Element, cls: string, data:string) : SVGElement { + return Svg.child(parent, "path", {class:cls, d:data}); + } + + static fill(el: SVGElement, c : string) { + (el).style.fill = c; + } + + static fills(els: SVGElement[], c : string) { + els.forEach(el => (el).style.fill = c); + } + + static buttonEvents(el : Element, + move: (ev: MouseEvent) => void, + start?: (ev:MouseEvent) => void, + stop?: (ev:MouseEvent) => void) { + let captured = false; + el.addEventListener('mousedown', (ev: MouseEvent) => { + captured = true; + if (start) start(ev) + return true; + }); + el.addEventListener('mousemove', (ev:MouseEvent) => { + if (captured) { + move(ev); + ev.preventDefault(); + return false; + } + return true; + }); + el.addEventListener('mouseup', (ev:MouseEvent) => { + captured = false; + if (stop) stop(ev); + }); + el.addEventListener('mouseleave', (ev:MouseEvent) => { + captured = false; + if (stop) stop(ev); + }); + } + + static linearGradient(defs: SVGDefsElement, id : string) : SVGLinearGradientElement { + let gradient = Svg.child(defs, "linearGradient", { id: id, x1:"0%", y1:"0%", x2:"0%", y2:"100%" }); + let stop1 = Svg.child(gradient, "stop", {offset:"0%"}) + let stop2 = Svg.child(gradient, "stop", {offset:"100%"}) + let stop3 = Svg.child(gradient, "stop", {offset:"100%"}) + let stop4 = Svg.child(gradient, "stop", {offset:"100%"}) + return gradient; + } + + static animate(el: SVGElement, cls: string) { + el.classList.add(cls); + let p = el.parentElement; + p.removeChild(el); + p.appendChild(el) + } + } + + export class MicrobitBoardSvg + { + public element : SVGSVGElement; + private defs : SVGDefsElement; + private g: SVGElement; + + private logos: SVGElement[]; + private head: SVGGElement; private headInitialized = false; + private headText: SVGTextElement; + private display: SVGElement; + private buttons: SVGElement[]; + private buttonsOuter: SVGElement[]; + private pins: SVGElement[]; + private pinGradients: SVGLinearGradientElement[]; + private pinTexts: SVGTextElement[]; + private ledsOuter: SVGElement[]; + private leds: SVGElement[]; + private systemLed: SVGCircleElement; + private antenna: SVGPolylineElement; + public board: rt.micro_bit.Board; + + constructor(public props: IBoardProps) { + this.board = this.props.runtime.board as rt.micro_bit.Board; + this.board.updateView = () => this.updateState(); + this.buildDom(); + this.updateTheme(); + this.updateState(); + this.attachEvents(); + } + + private updateTheme() { + let theme = this.props.theme; + + Svg.fill(this.display, theme.display); + Svg.fills(this.leds, theme.ledOn); + Svg.fills(this.ledsOuter, theme.ledOff); + Svg.fills(this.buttonsOuter, theme.buttonOuter); + Svg.fills(this.buttons, theme.buttonUp); + Svg.fills(this.logos, theme.accent); + + this.pinGradients.forEach(lg => { + (lg.childNodes[0]).style.stopColor = theme.pin; + (lg.childNodes[1]).style.stopColor = theme.pin; + (lg.childNodes[2]).style.stopColor = theme.pinActive; + (lg.childNodes[3]).style.stopColor = theme.pinActive; + }) + } + + public updateState() { + let state = this.board; + if (!state) return; + let theme = this.props.theme; + + state.buttons.forEach((btn, index) => { + Svg.fill(this.buttons[index], btn.pressed ? theme.buttonDown : theme.buttonUp); + }); + + var bw = state.displayMode == rt.micro_bit.DisplayMode.bw + var img = state.image; + this.leds.forEach((led,i) => { + var sel = (led) + sel.style.opacity = ((bw ? img.data[i] > 0 ? 255 : 0 : img.data[i]) / 255.0) + ""; + }) + this.updatePins(); + this.updateTilt(); + this.updateHeading(); + (this.buttonsOuter[2]).style.visibility = state.usesButtonAB ? 'visible' : 'hidden'; + (this.buttons[2]).style.visibility = state.usesButtonAB ? 'visible' : 'hidden'; + } + + private updatePin(pin : Pin, index: number) { + if (!pin) return; + let text = this.pinTexts[index]; + let v = ''; + if (pin.mode & PinMode.Analog) { + v = Math.floor(100 - (pin.value || 0) / 1023 * 100) + '%'; + if(text) text.textContent = (pin.value || 0) + ""; + } + else if (pin.mode & PinMode.Digital) { + v = pin.value > 0 ? '0%' : '100%'; + if (text) text.textContent = pin.value > 0 ? "1" : "0"; + } + else if (pin.mode & PinMode.Touch) { + v = pin.touched ? '0%' : '100%'; + if (text) text.textContent = ""; + } + if (v) { + let lg = this.pinGradients[index]; + (lg.childNodes[1]).setAttribute("offset", v); + (lg.childNodes[2]).setAttribute("offset", v); + } + } + + private updateHeading() { + let xc = 258; + let yc = 75; + let state = this.board; + if (!state || !state.usesHeading) return; + if (!this.headInitialized) { + let p = this.head.firstChild.nextSibling as SVGPathElement; + p.setAttribute("d", "m269.9,50.134647l0,0l-39.5,0l0,0c-14.1,0.1 -24.6,10.7 -24.6,24.8c0,13.9 10.4,24.4 24.3,24.7l0,0l39.6,0c14.2,0 40.36034,-22.97069 40.36034,-24.85394c0,-1.88326 -26.06034,-24.54606 -40.16034,-24.64606m-0.2,39l0,0l-39.3,0c-7.7,-0.1 -14,-6.4 -14,-14.2c0,-7.8 6.4,-14.2 14.2,-14.2l39.1,0c7.8,0 14.2,6.4 14.2,14.2c0,7.9 -6.4,14.2 -14.2,14.2l0,0l0,0z"); + let pt = this.element.createSVGPoint(); + Svg.buttonEvents( + this.head, + (ev : MouseEvent) => { + let cur = Svg.cursorPoint(pt, this.element, ev); + state.heading = Math.floor(Math.atan2(cur.y - yc, cur.x - xc) * 180 / Math.PI+90); + if (state.heading < 0) state.heading += 360; + console.log('heading: ' + state.heading) + this.updateHeading(); + }); + this.headInitialized = true; + } + + let txt = state.heading.toString() + '°'; + if (txt != this.headText.textContent) { + Svg.rotateElement(this.head, xc, yc, state.heading+180); + this.headText.textContent = txt; + } + } + + private lastFlashTime : number = 0; + public flashSystemLed() { + if (!this.systemLed) + this.systemLed = Svg.child(this.g, "circle", {class:"sim-systemled", cx:300, cy:20, r:5}) + let now = Date.now(); + if (now - this.lastFlashTime > 150) { + this.lastFlashTime = now; + Svg.animate(this.systemLed, 'sim-flash') + } + } + + private lastAntennaFlash : number = 0; + public flashAntenna() { + if (!this.antenna) { + let ax = 380; + let dax = 18; + let ayt = 10; + let ayb = 40; + this.antenna = Svg.child(this.g, "polyline", { class:"sim-antenna", points: `${ax},${ayb} ${ax},${ayt} ${ax+=dax},${ayt} ${ax},${ayb} ${ax+=dax},${ayb} ${ax},${ayt} ${ax+=dax},${ayt} ${ax},${ayb} ${ax+=dax},${ayb} ${ax},${ayt} ${ax+=dax},${ayt}`}) + } + let now = Date.now(); + if (now - this.lastAntennaFlash > 200) { + this.lastAntennaFlash = now; + Svg.animate(this.antenna, 'sim-flash-stroke') + } + } + + private updatePins() { + let state = this.board; + if (!state) return; + + state.pins.forEach((pin,i) => this.updatePin(pin,i)); + } + + private updateTilt() { + if (this.props.disableTilt) return; + let state = this.board; + if (!state || !state.usesAcceleration) return; + + var acc = state.acceleration; + var af = 8 / 1023; + if(acc && !isNaN(acc[0]) && !isNaN(acc[1])) { + this.element.style.transform = "perspective(30em) rotateX(" + -acc[1]*af + "deg) rotateY(" + acc[0]*af +"deg)" + this.element.style.perspectiveOrigin = "50% 50% 50%"; + this.element.style.perspective = "30em"; + } + } + + private buildDom() { + this.element = Svg.elt("svg") + Svg.hydrate(this.element, { + "version": "1.0", + "viewBox": "0 0 498 406", + "enable-background": "new 0 0 498 406", + "class":"sim", + "x": "0px", + "y": "0px"}); + this.defs = Svg.child(this.element, "defs", {}); + this.g = Svg.elt("g"); + this.element.appendChild(this.g); + + // outline + Svg.path(this.g, "sim-board", "M498,31.9C498,14.3,483.7,0,466.1,0H31.9C14.3,0,0,14.3,0,31.9v342.2C0,391.7,14.3,406,31.9,406h434.2c17.6,0,31.9-14.3,31.9-31.9V31.9z M14.3,206.7c-2.7,0-4.8-2.2-4.8-4.8c0-2.7,2.2-4.8,4.8-4.8c2.7,0,4.8,2.2,4.8,4.8C19.2,204.6,17,206.7,14.3,206.7z M486.2,206.7c-2.7,0-4.8-2.2-4.8-4.8c0-2.72.2-4.8,4.8-4.8c2.7,0,4.8,2.2,4.8,4.8C491,204.6,488.8,206.7,486.2,206.7z"); + + // script background + this.display = Svg.path(this.g, "sim-display", "M333.8,310.3H165.9c-8.3,0-15-6.7-15-15V127.5c0-8.3,6.7-15,15-15h167.8c8.3,0,15,6.7,15,15v167.8C348.8,303.6,342.1,310.3,333.8,310.3z"); + + this.logos = []; + this.logos.push(Svg.child(this.g, "polygon", {class:"sim-theme", points:"115,56.7 173.1,0 115,0"})); + this.logos.push(Svg.path(this.g, "sim-theme", "M114.2,0H25.9C12.1,2.1,0,13.3,0,27.7v83.9L114.2,0z")); + this.logos.push(Svg.child(this.g, "polygon", {class:"sim-theme", points:"173,27.9 202.5,0 173,0"})); + this.logos.push(Svg.child(this.g, "polygon", {class:"sim-theme", points:"54.1,242.4 54.1,274.1 22.4,274.1"})); + this.logos.push(Svg.child(this.g, "polygon", {class:"sim-theme", points:"446.2,164.6 446.2,132.8 477.9,132.8"})); + + // leds + this.leds = []; + this.ledsOuter = []; + var left = 154, top = 113, ledoffw = 46, ledoffh = 44; + for (var i = 0; i < 5; ++i) { + var ledtop = i * ledoffh + top; + for (var j = 0; j < 5; ++j) { + var ledleft = j * ledoffw + left; + var k = i * 5 + j; + this.ledsOuter.push(Svg.child(this.g, "rect", { class:"sim-led-back", x:ledleft, y:ledtop, width:10, height:20, rx:2, ry:2 })); + this.leds.push(Svg.child(this.g, "rect", { class:"sim-led", x:ledleft-2, y:ledtop-2, width:14, height:24, rx:2, ry:2})); + } + } + + // head + this.head = Svg.child(this.g, "g", {}); + Svg.child(this.head, "circle", { cx: 258, cy: 75, r: 100, fill:'transparent'}) + this.logos.push(Svg.path(this.head, "sim-theme","M269.9,50.2L269.9,50.2l-39.5,0v0c-14.1,0.1-24.6,10.7-24.6,24.8c0,13.9,10.4,24.4,24.3,24.7v0h39.6c14.2,0,24.8-10.6,24.8-24.7C294.5,61,284,50.3,269.9,50.2 M269.7,89.2L269.7,89.2l-39.3,0c-7.7-0.1-14-6.4-14-14.2c0-7.8,6.4-14.2,14.2-14.2h39.1c7.8,0,14.2,6.4,14.2,14.2C283.9,82.9,277.5,89.2,269.7,89.2")); + this.logos.push(Svg.path(this.head, "sim-theme","M230.6,69.7c-2.9,0-5.3,2.4-5.3,5.3c0,2.9,2.4,5.3,5.3,5.3c2.9,0,5.3-2.4,5.3-5.3C235.9,72.1,233.5,69.7,230.6,69.7")); + this.logos.push(Svg.path(this.head, "sim-theme","M269.7,80.3c2.9,0,5.3-2.4,5.3-5.3c0-2.9-2.4-5.3-5.3-5.3c-2.9,0-5.3,2.4-5.3,5.3C264.4,77.9,266.8,80.3,269.7,80.3")); + this.headText = Svg.child(this.g, "text", { x: 310, y: 100, class:'sim-text' }) + + // P0, P1, P2 + this.pins = [ + "M16.5,341.2c0,0.4-0.1,0.9-0.1,1.3v60.7c4.1,1.7,8.6,2.7,12.9,2.7h34.4v-64.7h0.3c0,0,0-0.1,0-0.1c0-13-10.6-23.6-23.7-23.6C27.2,317.6,16.5,328.1,16.5,341.2z M21.2,341.6c0-10.7,8.7-19.3,19.3-19.3c10.7,0,19.3,8.7,19.3,19.3c0,10.7-8.6,19.3-19.3,19.3C29.9,360.9,21.2,352.2,21.2,341.6z", + "M139.1,317.3c-12.8,0-22.1,10.3-23.1,23.1V406h46.2v-65.6C162.2,327.7,151.9,317.3,139.1,317.3zM139.3,360.1c-10.7,0-19.3-8.6-19.3-19.3c0-10.7,8.6-19.3,19.3-19.3c10.7,0,19.3,8.7,19.3,19.3C158.6,351.5,150,360.1,139.3,360.1z", + "M249,317.3c-12.8,0-22.1,10.3-23.1,23.1V406h46.2v-65.6C272.1,327.7,261.8,317.3,249,317.3z M249.4,360.1c-10.7,0-19.3-8.6-19.3-19.3c0-10.7,8.6-19.3,19.3-19.3c10.7,0,19.3,8.7,19.3,19.3C268.7,351.5,260.1,360.1,249.4,360.1z" + ].map(p => Svg.path(this.g, "sim-pin sim-pin-touch", p)); + + // side pins + this.pins.push(Svg.path(this.g, "sim-pin", "M0,357.7v19.2c0,10.8,6.2,20.2,14.4,25.2v-44.4H0z")); + this.pins.push(Svg.path(this.g, "sim-pin", "M483.6,402c8.2-5,14.4-14.4,14.4-25.1v-19.2h-14.4V402z")); + + [66.7,79.1,91.4,103.7,164.3,176.6,188.9,201.3,213.6,275.2,287.5,299.8,312.1,324.5,385.1,397.4,409.7,422].forEach(x => { + this.pins.push(Svg.child(this.g, "rect", {x:x, y:356.7, width:10, height:50})); + }) + + this.pins.push(Svg.path(this.g, "sim-pin", "M359.9,317.3c-12.8,0-22.1,10.3-23.1,23.1V406H383v-65.6C383,327.7,372.7,317.3,359.9,317.3z M360,360.1c-10.7,0-19.3-8.6-19.3-19.3c0-10.7,8.6-19.3,19.3-19.3c10.7,0,19.3,8.7,19.3,19.3C379.3,351.5,370.7,360.1,360,360.1z")); + this.pins.push(Svg.path(this.g, "sim-pin", "M458,317.6c-13,0-23.6,10.6-23.6,23.6c0,0,0,0.1,0,0.1h0V406H469c4.3,0,8.4-1,12.6-2.7v-60.7c0-0.4,0-0.9,0-1.3C481.6,328.1,471,317.6,458,317.6z M457.8,360.9c-10.7,0-19.3-8.6-19.3-19.3c0-10.7,8.6-19.3,19.3-19.3c10.7,0,19.3,8.7,19.3,19.3C477.1,352.2,468.4,360.9,457.8,360.9z")); + + this.pinGradients = this.pins.map((pin,i) => { + let gid= "gradient-pin-" + i + let lg = Svg.linearGradient(this.defs, gid) + pin.setAttribute("fill", `url(#${gid})`); + return lg; + }) + + this.pinTexts = [67,165,275].map(x => Svg.child(this.g, "text", { class:'sim-text-pin', x:x, y:345 })); + + this.buttonsOuter = []; this.buttons = []; + this.buttonsOuter.push(Svg.path(this.g, "sim-button-outer", "M82.1,232.6H25.9c-0.5,0-1-0.4-1-1v-56.2c0-0.5,0.4-1,1-1h56.2c0.5,0,1,0.4,1,1v56.2C83,232.2,82.6,232.6,82.1,232.6")); + this.buttons.push(Svg.path(this.g, "sim-button", "M69.7,203.5c0,8.7-7,15.7-15.7,15.7s-15.7-7-15.7-15.7c0-8.7,7-15.7,15.7-15.7S69.7,194.9,69.7,203.5")); + this.buttonsOuter.push(Svg.path(this.g, "sim-button-outer", "M474.3,232.6h-56.2c-0.5,0-1-0.4-1-1v-56.2c0-0.5,0.4-1,1-1h56.2c0.5,0,1,0.4,1,1v56.2C475.3,232.2,474.8,232.6,474.3,232.6")); + this.buttons.push(Svg.path(this.g, "sim-button", "M461.9,203.5c0,8.7-7,15.7-15.7,15.7c-8.7,0-15.7-7-15.7-15.7c0-8.7,7-15.7,15.7-15.7C454.9,187.8,461.9,194.9,461.9,203.5")); + this.buttonsOuter.push(Svg.child(this.g, "rect", {class:"sim-button-outer", x:417, y:250, width:58, height:58, rx:1, ry:1})); + this.buttons.push(Svg.child(this.g, "circle", {class:"sim-button", cx:446, cy:278, r:16.5})); + (this.buttonsOuter[2]).style.visibility = 'hidden'; + (this.buttons[2]).style.visibility = 'hidden'; + + + Svg.path(this.g, "sim-label", "M35.7,376.4c0-2.8,2.1-5.1,5.5-5.1c3.3,0,5.5,2.4,5.5,5.1v4.7c0,2.8-2.2,5.1-5.5,5.1c-3.3,0-5.5-2.4-5.5-5.1V376.4zM43.3,376.4c0-1.3-0.8-2.3-2.2-2.3c-1.3,0-2.1,1.1-2.1,2.3v4.7c0,1.2,0.8,2.3,2.1,2.3c1.3,0,2.2-1.1,2.2-2.3V376.4z"); + Svg.path(this.g, "sim-label", "M136.2,374.1c2.8,0,3.4-0.8,3.4-2.5h2.9v14.3h-3.4v-9.5h-3V374.1z"); + Svg.path(this.g, "sim-label", "M248.6,378.5c1.7-1,3-1.7,3-3.1c0-1.1-0.7-1.6-1.6-1.6c-1,0-1.8,0.6-1.8,2.1h-3.3c0-2.6,1.8-4.6,5.1-4.6c2.6,0,4.9,1.3,4.9,4.3c0,2.4-2.3,3.9-3.8,4.7c-2,1.3-2.5,1.8-2.5,2.9h6.1v2.7h-10C244.8,381.2,246.4,379.9,248.6,378.5z"); + Svg.path(this.g, "sim-label", "M48.1,270.9l-0.6-1.7h-5.1l-0.6,1.7h-3.5l5.1-14.3h3.1l5.2,14.3H48.1z M45,260.7l-1.8,5.9h3.5L45,260.7z"); + + Svg.path(this.g, "sim-label", "M449.1,135.8h5.9c3.9,0,4.7,2.4,4.7,3.9c0,1.8-1.4,2.9-2.5,3.2c0.9,0,2.6,1.1,2.6,3.3c0,1.5-0.8,4-4.7,4h-6V135.8zM454.4,141.7c1.6,0,2-1,2-1.7c0-0.6-0.3-1.7-2-1.7h-2v3.4H454.4z M452.4,144.1v3.5h2.1c1.6,0,2-1,2-1.8c0-0.7-0.4-1.8-2-1.8H452.4z") + Svg.path(this.g, "sim-label", "M352.1,381.1c0,1.6,0.9,2.5,2.2,2.5c1.2,0,1.9-0.9,1.9-1.9c0-1.2-0.6-2-2.1-2h-1.3v-2.6h1.3c1.5,0,1.9-0.7,1.9-1.8c0-1.1-0.7-1.6-1.6-1.6c-1.4,0-1.8,0.8-1.8,2.1h-3.3c0-2.4,1.5-4.6,5.1-4.6c2.6,0,5,1.3,5,4c0,1.6-1,2.8-2.1,3.2c1.3,0.5,2.3,1.6,2.3,3.5c0,2.7-2.4,4.3-5.2,4.3c-3.5,0-5.5-2.1-5.5-5.1H352.1z") + Svg.path(this.g, "sim-label", "M368.5,385.9h-3.1l-5.1-14.3h3.5l3.1,10.1l3.1-10.1h3.6L368.5,385.9z") + Svg.path(this.g, "sim-label", "M444.4,378.3h7.4v2.5h-1.5c-0.6,3.3-3,5.5-7.1,5.5c-4.8,0-7.5-3.5-7.5-7.5c0-3.9,2.8-7.5,7.5-7.5c3.8,0,6.4,2.3,6.6,5h-3.5c-0.2-1.1-1.4-2.2-3.1-2.2c-2.7,0-4.1,2.3-4.1,4.7c0,2.5,1.4,4.7,4.4,4.7c2,0,3.2-1.2,3.4-2.7h-2.5V378.3z") + Svg.path(this.g, "sim-label", "M461.4,380.9v-9.3h3.3v14.3h-3.5l-5.2-9.2v9.2h-3.3v-14.3h3.5L461.4,380.9z") + Svg.path(this.g, "sim-label", "M472.7,371.6c4.8,0,7.5,3.5,7.5,7.2s-2.7,7.2-7.5,7.2h-5.3v-14.3H472.7z M470.8,374.4v8.6h1.8c2.7,0,4.2-2.1,4.2-4.3s-1.6-4.3-4.2-4.3H470.8z") + } + + private attachEvents() { + Runtime.messagePosted = (msg) => { + switch(msg.type || '') { + case 'serial': this.flashSystemLed(); break; + case 'radiopacket': this.flashAntenna(); break; + } + } + this.element.addEventListener("mousemove", (ev: MouseEvent) => { + let state = this.board; + if (!state.acceleration) return; + let ax = (ev.clientX - this.element.clientWidth / 2) / (this.element.clientWidth / 3); + let ay = (ev.clientY - this.element.clientHeight / 2) / (this.element.clientHeight / 3); + state.acceleration[0] = Math.max(-1023, Math.min(1023, Math.floor(ax * 1023))); + state.acceleration[1] = Math.max(-1023, Math.min(1023, Math.floor(ay * 1023))); + state.acceleration[2] = Math.floor(Math.sqrt(1023*1023 + - state.acceleration[0] *state.acceleration[0] + - state.acceleration[1] *state.acceleration[1])); + this.updateTilt(); + }, false); + this.element.addEventListener("mouseleave", (ev: MouseEvent) => { + let state = this.board; + if (!state.acceleration) return; + + state.acceleration[0] = 0; + state.acceleration[1] = 0; + state.acceleration[2] = -1023; + this.updateTilt(); + }, false); + + this.pins.forEach((pin, index) => { + if (!this.board.pins[index]) return; + let pt = this.element.createSVGPoint(); + Svg.buttonEvents(pin, + // move + ev => { + let state = this.board; + let pin = state.pins[index]; + let svgpin = this.pins[index]; + if (pin.mode & PinMode.Input) { + let cursor = Svg.cursorPoint(pt, this.element, ev); + let v = (400 - cursor.y) / 40 * 1023 + pin.value = Math.max(0, Math.min(1023, Math.floor(v))); + } + this.updatePin(pin,index); + }, + // start + ev => { + let state = this.board; + let pin = state.pins[index]; + let svgpin = this.pins[index]; + svgpin.classList.add('touched'); + if (pin.mode & PinMode.Touch) { + pin.touched = true; + } else if (pin.mode & PinMode.Input) { + let cursor = Svg.cursorPoint(pt, this.element, ev); + let v = (400 - cursor.y) / 40 * 1023 + pin.value = Math.max(0, Math.min(1023, Math.floor(v))); + } + this.updatePin(pin,index); + }, + // stop + (ev: MouseEvent) => { + let state = this.board; + let pin = state.pins[index]; + let svgpin = this.pins[index]; + svgpin.classList.remove('touched'); + if (pin.mode & PinMode.Touch) { + pin.touched = false; + let ens = enums(); + this.board.bus.queue(pin.id, ens.MICROBIT_BUTTON_EVT_CLICK); + } + this.updatePin(pin, index); + return false; + }); + }) + this.buttonsOuter.slice(0,2).forEach((btn, index) => { + btn.addEventListener("mousedown", ev => { + let state = this.board; + state.buttons[index].pressed = true; + Svg.fill(this.buttons[index], this.props.theme.buttonDown); + }) + btn.addEventListener("mouseup", ev => { + let state = this.board; + state.buttons[index].pressed = false; + Svg.fill(this.buttons[index], this.props.theme.buttonUp); + + let ens = enums(); + this.board.bus.queue(state.buttons[index].id, ens.MICROBIT_BUTTON_EVT_CLICK); + }) + }) + this.buttonsOuter[2].addEventListener("mousedown", ev => { + let state = this.board; + state.buttons[0].pressed = true; + state.buttons[1].pressed = true; + state.buttons[2].pressed = true; + Svg.fill(this.buttons[0], this.props.theme.buttonDown); + Svg.fill(this.buttons[1], this.props.theme.buttonDown); + Svg.fill(this.buttons[2], this.props.theme.buttonDown); + }) + this.buttonsOuter[2].addEventListener("mouseup", ev => { + let state = this.board; + state.buttons[0].pressed = false; + state.buttons[1].pressed = false; + state.buttons[2].pressed = false; + Svg.fill(this.buttons[0], this.props.theme.buttonUp); + Svg.fill(this.buttons[1], this.props.theme.buttonUp); + Svg.fill(this.buttons[2], this.props.theme.buttonUp); + + let ens = enums(); + this.board.bus.queue(state.buttons[2].id, ens.MICROBIT_BUTTON_EVT_CLICK); + }) + } + } +} \ No newline at end of file diff --git a/sim/state.ts b/sim/state.ts new file mode 100644 index 00000000..539dca67 --- /dev/null +++ b/sim/state.ts @@ -0,0 +1,348 @@ +namespace ks.rt.micro_bit { + export interface RuntimeOptions { + theme: string; + } + + export enum DisplayMode { + bw, + greyscale + } + + export enum PinMode { + Digital = 0x0001, + Analog = 0x0002, + Input = 0x0004, + Output = 0x0008, + Touch = 0x0010 + } + + export class Pin { + constructor(public id: number) {} + touched = false; + value = 0; + mode = PinMode.Digital | PinMode.Output; + + isTouched() : boolean { + this.mode = PinMode.Touch; + return this.touched; + } + } + + export class Button { + constructor(public id : number) {} + pressed: boolean; + } + + export class EventBus { + private queues : Map> = {}; + + constructor(private runtime : Runtime) { } + + listen(id:number, evid:number, handler: RefAction) { + let k = id + ':' + evid; + let queue = this.queues[k]; + if (!queue) queue = this.queues[k] = new EventQueue(this.runtime); + queue.handler = handler; + } + + queue(id: number, evid: number, value: number = 0) { + let k = id + ':' + evid; + let queue = this.queues[k]; + if (queue) queue.push(value); + } + } + + export interface PacketBuffer { + data:number[]; + rssi?:number; + } + + export class RadioDatagram { + datagram: PacketBuffer[] = []; + lastReceived: PacketBuffer = { + data:[0,0,0,0], + rssi: -1 + }; + + constructor(private runtime : Runtime) { + } + + queue(packet : PacketBuffer) { + if (this.datagram.length < 5) { + this.datagram.push(packet); + let ens = enums(); + (runtime.board).bus.queue(ens.MICROBIT_ID_RADIO, ens.MICROBIT_RADIO_EVT_DATAGRAM); + } + } + + send(buffer : number[]) { + Runtime.postMessage({ + type:'radiopacket', + data: buffer.slice(0, 8) + }) + } + + recv() : PacketBuffer { + var r = this.datagram.shift(); + if (!r) r = { + data:[0,0,0,0], + rssi: -1 + }; + return this.lastReceived = r; + } + } + + export class RadioBus { + // uint8_t radioDefaultGroup = MICROBIT_RADIO_DEFAULT_GROUP; + groupId = 0; // todo + datagram : RadioDatagram; + + constructor(private runtime : Runtime) { + this.datagram = new RadioDatagram(runtime); + } + + setGroup(id: number) { + this.groupId = id & 0xff; // byte only + } + + broadcast(msg: number) { + let ens = enums(); + Runtime.postMessage({ + type:'eventbus', + id: ens.MES_BROADCAST_GENERAL_ID, + eventid: msg + }) + } + } + + export interface SimulatorEventBusMessage extends SimulatorMessage { + id: number; + eventid: number; + value?: number; + } + + export interface SimulatorSerialMessage extends SimulatorMessage { + id:string; + data: string; + } + + export interface SimulatorRadioPacketMessage extends SimulatorMessage { + data: number[]; + rssi?: number; + } + + export class Board extends BaseBoard { + id: string; + + // the bus + bus : EventBus; + radio: RadioBus; + + // display + image = createImage(5); + brigthness = 255; + displayMode = DisplayMode.bw; + font: Image = createFont(); + + // buttons + usesButtonAB: boolean = false; + buttons : Button[]; + + // pins + pins : Pin[]; + + // serial + serialIn: string[] = []; + + // sensors + usesAcceleration = false; + acceleration = [0, 0, -1023]; + + usesHeading = false; + heading = 90; + + temperature = 21; + lightLevel = 128; + + animationQ: AnimationQueue; + + constructor() { + super() + this.id = "b" + Math.random(); + this.animationQ = new AnimationQueue(runtime); + this.bus = new EventBus(runtime); + this.radio = new RadioBus(runtime); + let ens = enums(); + this.buttons = [ + new Button(ens.MICROBIT_ID_BUTTON_A), + new Button(ens.MICROBIT_ID_BUTTON_B), + new Button(ens.MICROBIT_ID_BUTTON_AB) + ]; + this.pins = [ + new Pin(ens.MICROBIT_ID_IO_P0), + new Pin(ens.MICROBIT_ID_IO_P1), + new Pin(ens.MICROBIT_ID_IO_P2), + new Pin(ens.MICROBIT_ID_IO_P3), + new Pin(ens.MICROBIT_ID_IO_P4), + new Pin(ens.MICROBIT_ID_IO_P5), + new Pin(ens.MICROBIT_ID_IO_P6), + new Pin(ens.MICROBIT_ID_IO_P7), + new Pin(ens.MICROBIT_ID_IO_P8), + new Pin(ens.MICROBIT_ID_IO_P9), + new Pin(ens.MICROBIT_ID_IO_P10), + new Pin(ens.MICROBIT_ID_IO_P11), + new Pin(ens.MICROBIT_ID_IO_P12), + new Pin(ens.MICROBIT_ID_IO_P13), + new Pin(ens.MICROBIT_ID_IO_P14), + new Pin(ens.MICROBIT_ID_IO_P15), + new Pin(ens.MICROBIT_ID_IO_P16), + null, + null, + new Pin(ens.MICROBIT_ID_IO_P19), + new Pin(ens.MICROBIT_ID_IO_P20) + ]; + } + + + initAsync(msg : SimulatorRunMessage) : Promise { + let options = (msg.options || {}) as RuntimeOptions; + let theme : micro_bit.IBoardTheme; + switch(options.theme) { + case 'blue': theme = micro_bit.themes[0]; break; + case 'yellow': theme = micro_bit.themes[1]; break; + case 'green': theme = micro_bit.themes[2]; break; + case 'red': theme = micro_bit.themes[3]; break; + default: theme = ks.rt.micro_bit.randomTheme(); + } + + console.log('setting up microbit simulator') + let view = new ks.rt.micro_bit.MicrobitBoardSvg({ + theme: theme, + runtime: runtime + }) + document.body.innerHTML = ''; // clear children + document.body.appendChild(view.element); + + return Promise.resolve(); + } + + 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': + this.serialIn.push((msg).data || ''); + break; + case 'radiopacket': + let packet = msg; + this.radio.datagram.queue({ data: packet.data || [], rssi: packet.rssi || 0}) + break; + } + } + + readSerial() { + let v = this.serialIn.shift() || ''; + return v; + } + + serialOutBuffer: string = ''; + writeSerial(s : string) { + for(let i = 0; i < s.length;++i) { + let c = s[i]; + switch(c) { + case '\n': + Runtime.postMessage({ + type: 'serial', + data: this.serialOutBuffer, + id: runtime.id + }) + this.serialOutBuffer = '' + break; + case '\r': continue; + default: this.serialOutBuffer += c; + } + } + } + } + + export class Image { + public width: number; + public data: number[]; + constructor(width: number, data: number[]) { + this.width = width; + this.data = data; + } + public get(x: number, y: number): number { + // TODO range checking + return this.data[y * this.width + x]; + } + public set(x: number, y: number, v: number) { + // TODO range checking + this.data[y * this.width + x] = v; + } + public copyTo(xSrcIndex: number, length: number, target: Image, xTargetIndex: number): void { + for (let x = 0; x < length; x++) { + for (let y = 0; y < 5; y++) { + let value = this.get(xSrcIndex + x, y); + target.set(xTargetIndex + x, y, value); + } + } + } + public clear() : void { + for(var i = 0;i < this.data.length; ++i) + this.data[i] = 0; + } + } + + export function createImage(width: number): Image { + return new Image(width, new Array(width * 5)); + } + + export function createImageFromBuffer(data: number[]): Image { + return new Image(data.length / 5, data); + } + + export function createImageFromString(text: string): Image { + let font = board().font; + let w = font.width; + let sprite = createImage(6 * text.length - 1); + let k = 0; + for (let i = 0; i < text.length; i++) { + let charCode = text.charCodeAt(i); + let charStart = (charCode - 32) * 5; + if (charStart < 0 || charStart + 5 > w) { + charCode = " ".charCodeAt(0); + charStart = (charCode - 32) * 5; + } + + font.copyTo(charStart, 5, sprite, k); + k = k + 5; + if (i < text.length - 1) { + k = k + 1; + } + } + return sprite; + } + + export function createFont(): Image { + var data = [0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x8, 0x8, 0x0, 0x8, 0xa, 0x4a, 0x40, 0x0, 0x0, 0xa, 0x5f, 0xea, 0x5f, 0xea, 0xe, 0xd9, 0x2e, 0xd3, 0x6e, 0x19, 0x32, 0x44, 0x89, 0x33, 0xc, 0x92, 0x4c, 0x92, 0x4d, 0x8, 0x8, 0x0, 0x0, 0x0, 0x4, 0x88, 0x8, 0x8, 0x4, 0x8, 0x4, 0x84, 0x84, 0x88, 0x0, 0xa, 0x44, 0x8a, 0x40, 0x0, 0x4, 0x8e, 0xc4, 0x80, 0x0, 0x0, 0x0, 0x4, 0x88, 0x0, 0x0, 0xe, 0xc0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x1, 0x22, 0x44, 0x88, 0x10, 0xc, 0x92, 0x52, 0x52, 0x4c, 0x4, 0x8c, 0x84, 0x84, 0x8e, 0x1c, 0x82, 0x4c, 0x90, 0x1e, 0x1e, 0xc2, 0x44, 0x92, 0x4c, 0x6, 0xca, 0x52, 0x5f, 0xe2, 0x1f, 0xf0, 0x1e, 0xc1, 0x3e, 0x2, 0x44, 0x8e, 0xd1, 0x2e, 0x1f, 0xe2, 0x44, 0x88, 0x10, 0xe, 0xd1, 0x2e, 0xd1, 0x2e, 0xe, 0xd1, 0x2e, 0xc4, 0x88, 0x0, 0x8, 0x0, 0x8, 0x0, 0x0, 0x4, 0x80, 0x4, 0x88, 0x2, 0x44, 0x88, 0x4, 0x82, 0x0, 0xe, 0xc0, 0xe, 0xc0, 0x8, 0x4, 0x82, 0x44, 0x88, 0xe, 0xd1, 0x26, 0xc0, 0x4, 0xe, 0xd1, 0x35, 0xb3, 0x6c, 0xc, 0x92, 0x5e, 0xd2, 0x52, 0x1c, 0x92, 0x5c, 0x92, 0x5c, 0xe, 0xd0, 0x10, 0x10, 0xe, 0x1c, 0x92, 0x52, 0x52, 0x5c, 0x1e, 0xd0, 0x1c, 0x90, 0x1e, 0x1e, 0xd0, 0x1c, 0x90, 0x10, 0xe, 0xd0, 0x13, 0x71, 0x2e, 0x12, 0x52, 0x5e, 0xd2, 0x52, 0x1c, 0x88, 0x8, 0x8, 0x1c, 0x1f, 0xe2, 0x42, 0x52, 0x4c, 0x12, 0x54, 0x98, 0x14, 0x92, 0x10, 0x10, 0x10, 0x10, 0x1e, 0x11, 0x3b, 0x75, 0xb1, 0x31, 0x11, 0x39, 0x35, 0xb3, 0x71, 0xc, 0x92, 0x52, 0x52, 0x4c, 0x1c, 0x92, 0x5c, 0x90, 0x10, 0xc, 0x92, 0x52, 0x4c, 0x86, 0x1c, 0x92, 0x5c, 0x92, 0x51, 0xe, 0xd0, 0xc, 0x82, 0x5c, 0x1f, 0xe4, 0x84, 0x84, 0x84, 0x12, 0x52, 0x52, 0x52, 0x4c, 0x11, 0x31, 0x31, 0x2a, 0x44, 0x11, 0x31, 0x35, 0xbb, 0x71, 0x12, 0x52, 0x4c, 0x92, 0x52, 0x11, 0x2a, 0x44, 0x84, 0x84, 0x1e, 0xc4, 0x88, 0x10, 0x1e, 0xe, 0xc8, 0x8, 0x8, 0xe, 0x10, 0x8, 0x4, 0x82, 0x41, 0xe, 0xc2, 0x42, 0x42, 0x4e, 0x4, 0x8a, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1f, 0x8, 0x4, 0x80, 0x0, 0x0, 0x0, 0xe, 0xd2, 0x52, 0x4f, 0x10, 0x10, 0x1c, 0x92, 0x5c, 0x0, 0xe, 0xd0, 0x10, 0xe, 0x2, 0x42, 0x4e, 0xd2, 0x4e, 0xc, 0x92, 0x5c, 0x90, 0xe, 0x6, 0xc8, 0x1c, 0x88, 0x8, 0xe, 0xd2, 0x4e, 0xc2, 0x4c, 0x10, 0x10, 0x1c, 0x92, 0x52, 0x8, 0x0, 0x8, 0x8, 0x8, 0x2, 0x40, 0x2, 0x42, 0x4c, 0x10, 0x14, 0x98, 0x14, 0x92, 0x8, 0x8, 0x8, 0x8, 0x6, 0x0, 0x1b, 0x75, 0xb1, 0x31, 0x0, 0x1c, 0x92, 0x52, 0x52, 0x0, 0xc, 0x92, 0x52, 0x4c, 0x0, 0x1c, 0x92, 0x5c, 0x90, 0x0, 0xe, 0xd2, 0x4e, 0xc2, 0x0, 0xe, 0xd0, 0x10, 0x10, 0x0, 0x6, 0xc8, 0x4, 0x98, 0x8, 0x8, 0xe, 0xc8, 0x7, 0x0, 0x12, 0x52, 0x52, 0x4f, 0x0, 0x11, 0x31, 0x2a, 0x44, 0x0, 0x11, 0x31, 0x35, 0xbb, 0x0, 0x12, 0x4c, 0x8c, 0x92, 0x0, 0x11, 0x2a, 0x44, 0x98, 0x0, 0x1e, 0xc4, 0x88, 0x1e, 0x6, 0xc4, 0x8c, 0x84, 0x86, 0x8, 0x8, 0x8, 0x8, 0x8, 0x18, 0x8, 0xc, 0x88, 0x18, 0x0, 0x0, 0xc, 0x83, 0x60]; + + let nb = data.length; + let n = nb / 5; + var font = createImage(nb); + for (let c = 0; c < n; c++) { + for (let row = 0; row < 5; row++) { + let char = data[c * 5 + row]; + for (let col = 0; col < 5; col++) { + if ((char & (1 << col)) != 0) + font.set((c * 5 + 4) - col, row, 255); + } + } + } + return font; + } +} \ No newline at end of file diff --git a/sim/tsconfig.json b/sim/tsconfig.json new file mode 100644 index 00000000..761e4ba9 --- /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": true + } +}