diff --git a/sim/state.ts b/sim/state.ts deleted file mode 100644 index 253101d4..00000000 --- a/sim/state.ts +++ /dev/null @@ -1,711 +0,0 @@ -namespace pxsim { - export interface RuntimeOptions { - theme: string; - } - - export enum DisplayMode { - bw, - greyscale - } - - export enum PinFlags { - Unused = 0, - Digital = 0x0001, - Analog = 0x0002, - Input = 0x0004, - Output = 0x0008, - Touch = 0x0010 - } - - export class Pin { - constructor(public id: number) { } - touched = false; - value = 0; - period = 0; - mode = PinFlags.Unused; - pitch = false; - pull = 0; // PullDown - - isTouched(): boolean { - this.mode = PinFlags.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[] | string; - 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 < 4) - this.datagram.push(packet); - (runtime.board).bus.queue(DAL.MICROBIT_ID_RADIO, DAL.MICROBIT_RADIO_EVT_DATAGRAM); - } - - send(buffer: number[] | string) { - if (buffer instanceof String) buffer = buffer.slice(0, 32); - else buffer = buffer.slice(0, 8); - - Runtime.postMessage({ - type: "radiopacket", - data: buffer - }) - } - - recv(): PacketBuffer { - let 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 - power = 0; - transmitSerialNumber = false; - datagram: RadioDatagram; - - constructor(private runtime: Runtime) { - this.datagram = new RadioDatagram(runtime); - } - - setGroup(id: number) { - this.groupId = id & 0xff; // byte only - } - - setTransmitPower(power: number) { - this.power = Math.max(0, Math.min(7, power)); - } - - setTransmitSerialNumber(sn: boolean) { - this.transmitSerialNumber = !!sn; - } - - broadcast(msg: number) { - Runtime.postMessage({ - type: "eventbus", - id: DAL.MES_BROADCAST_GENERAL_ID, - eventid: msg, - power: this.power, - group: this.groupId - }) - } - } - - interface AccelerometerSample { - x: number; - y: number; - z: number; - } - - interface ShakeHistory { - x: boolean; - y: boolean; - z: boolean; - count: number; - shaken: number; - timer: number; - } - - /** - * Co-ordinate systems that can be used. - * RAW: Unaltered data. Data will be returned directly from the accelerometer. - * - * SIMPLE_CARTESIAN: Data will be returned based on an easy to understand alignment, consistent with the cartesian system taught in schools. - * When held upright, facing the user: - * - * / - * +--------------------+ z - * | | - * | ..... | - * | * ..... * | - * ^ | ..... | - * | | | - * y +--------------------+ x--> - * - * - * NORTH_EAST_DOWN: Data will be returned based on the industry convention of the North East Down (NED) system. - * When held upright, facing the user: - * - * z - * +--------------------+ / - * | | - * | ..... | - * | * ..... * | - * ^ | ..... | - * | | | - * x +--------------------+ y--> - * - */ - export enum MicroBitCoordinateSystem { - RAW, - SIMPLE_CARTESIAN, - NORTH_EAST_DOWN - } - - export class Accelerometer { - private sigma: number = 0; // the number of ticks that the instantaneous gesture has been stable. - private lastGesture: number = 0; // the last, stable gesture recorded. - private currentGesture: number = 0 // the instantaneous, unfiltered gesture detected. - private sample: AccelerometerSample = { x: 0, y: 0, z: -1023 } - private shake: ShakeHistory = { x: false, y: false, z: false, count: 0, shaken: 0, timer: 0 }; // State information needed to detect shake events. - private pitch: number; - private roll: number; - private id: number; - public isActive = false; - public sampleRange = 2; - - constructor(public runtime: Runtime) { - this.id = DAL.MICROBIT_ID_ACCELEROMETER; - } - - public setSampleRange(range: number) { - this.activate(); - this.sampleRange = Math.max(1, Math.min(8, range)); - } - - public activate() { - if (!this.isActive) { - this.isActive = true; - this.runtime.queueDisplayUpdate(); - } - } - - /** - * Reads the acceleration data from the accelerometer, and stores it in our buffer. - * This is called by the tick() member function, if the interrupt is set! - */ - public update(x: number, y: number, z: number) { - // read MSB values... - this.sample.x = Math.floor(x); - this.sample.y = Math.floor(y); - this.sample.z = Math.floor(z); - - // Update gesture tracking - this.updateGesture(); - - // Indicate that a new sample is available - board().bus.queue(this.id, DAL.MICROBIT_ACCELEROMETER_EVT_DATA_UPDATE) - } - - public instantaneousAccelerationSquared() { - // Use pythagoras theorem to determine the combined force acting on the device. - return this.sample.x * this.sample.x + this.sample.y * this.sample.y + this.sample.z * this.sample.z; - } - - /** - * Service function. Determines the best guess posture of the device based on instantaneous data. - * This makes no use of historic data (except for shake), and forms this input to the filter implemented in updateGesture(). - * - * @return A best guess of the current posture of the device, based on instantaneous data. - */ - private instantaneousPosture(): number { - let force = this.instantaneousAccelerationSquared(); - let shakeDetected = false; - - // Test for shake events. - // We detect a shake by measuring zero crossings in each axis. In other words, if we see a strong acceleration to the left followed by - // a string acceleration to the right, then we can infer a shake. Similarly, we can do this for each acxis (left/right, up/down, in/out). - // - // If we see enough zero crossings in succession (MICROBIT_ACCELEROMETER_SHAKE_COUNT_THRESHOLD), then we decide that the device - // has been shaken. - if ((this.getX() < -DAL.MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && this.shake.x) || (this.getX() > DAL.MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && !this.shake.x)) { - shakeDetected = true; - this.shake.x = !this.shake.x; - } - - if ((this.getY() < -DAL.MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && this.shake.y) || (this.getY() > DAL.MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && !this.shake.y)) { - shakeDetected = true; - this.shake.y = !this.shake.y; - } - - if ((this.getZ() < -DAL.MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && this.shake.z) || (this.getZ() > DAL.MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && !this.shake.z)) { - shakeDetected = true; - this.shake.z = !this.shake.z; - } - - if (shakeDetected && this.shake.count < DAL.MICROBIT_ACCELEROMETER_SHAKE_COUNT_THRESHOLD && ++this.shake.count == DAL.MICROBIT_ACCELEROMETER_SHAKE_COUNT_THRESHOLD) - this.shake.shaken = 1; - - if (++this.shake.timer >= DAL.MICROBIT_ACCELEROMETER_SHAKE_DAMPING) { - this.shake.timer = 0; - if (this.shake.count > 0) { - if (--this.shake.count == 0) - this.shake.shaken = 0; - } - } - - if (this.shake.shaken) - return DAL.MICROBIT_ACCELEROMETER_EVT_SHAKE; - - let sq = (n: number) => n * n - - if (force < sq(DAL.MICROBIT_ACCELEROMETER_FREEFALL_TOLERANCE)) - return DAL.MICROBIT_ACCELEROMETER_EVT_FREEFALL; - - if (force > sq(DAL.MICROBIT_ACCELEROMETER_3G_TOLERANCE)) - return DAL.MICROBIT_ACCELEROMETER_EVT_3G; - - if (force > sq(DAL.MICROBIT_ACCELEROMETER_6G_TOLERANCE)) - return DAL.MICROBIT_ACCELEROMETER_EVT_6G; - - if (force > sq(DAL.MICROBIT_ACCELEROMETER_8G_TOLERANCE)) - return DAL.MICROBIT_ACCELEROMETER_EVT_8G; - - // Determine our posture. - if (this.getX() < (-1000 + DAL.MICROBIT_ACCELEROMETER_TILT_TOLERANCE)) - return DAL.MICROBIT_ACCELEROMETER_EVT_TILT_LEFT; - - if (this.getX() > (1000 - DAL.MICROBIT_ACCELEROMETER_TILT_TOLERANCE)) - return DAL.MICROBIT_ACCELEROMETER_EVT_TILT_RIGHT; - - if (this.getY() < (-1000 + DAL.MICROBIT_ACCELEROMETER_TILT_TOLERANCE)) - return DAL.MICROBIT_ACCELEROMETER_EVT_TILT_DOWN; - - if (this.getY() > (1000 - DAL.MICROBIT_ACCELEROMETER_TILT_TOLERANCE)) - return DAL.MICROBIT_ACCELEROMETER_EVT_TILT_UP; - - if (this.getZ() < (-1000 + DAL.MICROBIT_ACCELEROMETER_TILT_TOLERANCE)) - return DAL.MICROBIT_ACCELEROMETER_EVT_FACE_UP; - - if (this.getZ() > (1000 - DAL.MICROBIT_ACCELEROMETER_TILT_TOLERANCE)) - return DAL.MICROBIT_ACCELEROMETER_EVT_FACE_DOWN; - - return 0; - } - - updateGesture() { - // Determine what it looks like we're doing based on the latest sample... - let g = this.instantaneousPosture(); - - // Perform some low pass filtering to reduce jitter from any detected effects - if (g == this.currentGesture) { - if (this.sigma < DAL.MICROBIT_ACCELEROMETER_GESTURE_DAMPING) - this.sigma++; - } - else { - this.currentGesture = g; - this.sigma = 0; - } - - // If we've reached threshold, update our record and raise the relevant event... - if (this.currentGesture != this.lastGesture && this.sigma >= DAL.MICROBIT_ACCELEROMETER_GESTURE_DAMPING) { - this.lastGesture = this.currentGesture; - board().bus.queue(DAL.MICROBIT_ID_GESTURE, this.lastGesture); - } - } - - /** - * Reads the X axis value of the latest update from the accelerometer. - * @param system The coordinate system to use. By default, a simple cartesian system is provided. - * @return The force measured in the X axis, in milli-g. - * - * Example: - * @code - * uBit.accelerometer.getX(); - * uBit.accelerometer.getX(RAW); - * @endcode - */ - public getX(system: MicroBitCoordinateSystem = MicroBitCoordinateSystem.SIMPLE_CARTESIAN): number { - this.activate(); - switch (system) { - case MicroBitCoordinateSystem.SIMPLE_CARTESIAN: - return -this.sample.x; - - case MicroBitCoordinateSystem.NORTH_EAST_DOWN: - return this.sample.y; - //case MicroBitCoordinateSystem.SIMPLE_CARTESIAN.RAW: - default: - return this.sample.x; - } - } - - /** - * Reads the Y axis value of the latest update from the accelerometer. - * @param system The coordinate system to use. By default, a simple cartesian system is provided. - * @return The force measured in the Y axis, in milli-g. - * - * Example: - * @code - * uBit.accelerometer.getY(); - * uBit.accelerometer.getY(RAW); - * @endcode - */ - public getY(system: MicroBitCoordinateSystem = MicroBitCoordinateSystem.SIMPLE_CARTESIAN): number { - this.activate(); - switch (system) { - case MicroBitCoordinateSystem.SIMPLE_CARTESIAN: - return -this.sample.y; - - case MicroBitCoordinateSystem.NORTH_EAST_DOWN: - return -this.sample.x; - //case RAW: - default: - return this.sample.y; - } - } - - /** - * Reads the Z axis value of the latest update from the accelerometer. - * @param system The coordinate system to use. By default, a simple cartesian system is provided. - * @return The force measured in the Z axis, in milli-g. - * - * Example: - * @code - * uBit.accelerometer.getZ(); - * uBit.accelerometer.getZ(RAW); - * @endcode - */ - public getZ(system: MicroBitCoordinateSystem = MicroBitCoordinateSystem.SIMPLE_CARTESIAN): number { - this.activate(); - switch (system) { - case MicroBitCoordinateSystem.NORTH_EAST_DOWN: - return -this.sample.z; - //case MicroBitCoordinateSystem.SIMPLE_CARTESIAN: - //case MicroBitCoordinateSystem.RAW: - default: - return this.sample.z; - } - } - - /** - * Provides a rotation compensated pitch of the device, based on the latest update from the accelerometer. - * @return The pitch of the device, in degrees. - * - * Example: - * @code - * uBit.accelerometer.getPitch(); - * @endcode - */ - public getPitch(): number { - this.activate(); - return Math.floor((360 * this.getPitchRadians()) / (2 * Math.PI)); - } - - getPitchRadians(): number { - this.recalculatePitchRoll(); - return this.pitch; - } - - /** - * Provides a rotation compensated roll of the device, based on the latest update from the accelerometer. - * @return The roll of the device, in degrees. - * - * Example: - * @code - * uBit.accelerometer.getRoll(); - * @endcode - */ - public getRoll(): number { - this.activate(); - return Math.floor((360 * this.getRollRadians()) / (2 * Math.PI)); - } - - getRollRadians(): number { - this.recalculatePitchRoll(); - return this.roll; - } - - /** - * Recalculate roll and pitch values for the current sample. - * We only do this at most once per sample, as the necessary trigonemteric functions are rather - * heavyweight for a CPU without a floating point unit... - */ - recalculatePitchRoll() { - let x = this.getX(MicroBitCoordinateSystem.NORTH_EAST_DOWN); - let y = this.getY(MicroBitCoordinateSystem.NORTH_EAST_DOWN); - let z = this.getZ(MicroBitCoordinateSystem.NORTH_EAST_DOWN); - - this.roll = Math.atan2(y, z); - this.pitch = Math.atan(-x / (y * Math.sin(this.roll) + z * Math.cos(this.roll))); - } - - } - - - export class Board extends BaseBoard { - id: string; - - // the bus - bus: EventBus; - radio: RadioBus; - - // display - image = createInternalImage(5); - brigthness = 255; - displayMode = DisplayMode.bw; - font: Image = createFont(); - - // buttons - usesButtonAB: boolean = false; - buttons: Button[]; - - // pins - pins: Pin[]; - - // serial - serialIn: string[] = []; - - // sensors - accelerometer: Accelerometer; - - // gestures - useShake = false; - - usesHeading = false; - heading = 90; - - usesTemperature = false; - temperature = 21; - - usesLightLevel = false; - lightLevel = 128; - - animationQ: AnimationQueue; - - constructor() { - super() - this.id = "b" + Math_.random(2147483647); - this.animationQ = new AnimationQueue(runtime); - this.bus = new EventBus(runtime); - this.radio = new RadioBus(runtime); - this.accelerometer = new Accelerometer(runtime); - this.buttons = [ - new Button(DAL.MICROBIT_ID_BUTTON_A), - new Button(DAL.MICROBIT_ID_BUTTON_B), - new Button(DAL.MICROBIT_ID_BUTTON_AB) - ]; - this.pins = [ - new Pin(DAL.MICROBIT_ID_IO_P0), - new Pin(DAL.MICROBIT_ID_IO_P1), - new Pin(DAL.MICROBIT_ID_IO_P2), - new Pin(DAL.MICROBIT_ID_IO_P3), - new Pin(DAL.MICROBIT_ID_IO_P4), - new Pin(DAL.MICROBIT_ID_IO_P5), - new Pin(DAL.MICROBIT_ID_IO_P6), - new Pin(DAL.MICROBIT_ID_IO_P7), - new Pin(DAL.MICROBIT_ID_IO_P8), - new Pin(DAL.MICROBIT_ID_IO_P9), - new Pin(DAL.MICROBIT_ID_IO_P10), - new Pin(DAL.MICROBIT_ID_IO_P11), - new Pin(DAL.MICROBIT_ID_IO_P12), - new Pin(DAL.MICROBIT_ID_IO_P13), - new Pin(DAL.MICROBIT_ID_IO_P14), - new Pin(DAL.MICROBIT_ID_IO_P15), - new Pin(DAL.MICROBIT_ID_IO_P16), - null, - null, - new Pin(DAL.MICROBIT_ID_IO_P19), - new Pin(DAL.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 = pxsim.micro_bit.randomTheme(); - } - - let view = new pxsim.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; - } - - kill() { - super.kill(); - AudioContextManager.stop(); - } - - serialOutBuffer: string = ""; - writeSerial(s: string) { - for (let i = 0; i < s.length; ++i) { - let c = s[i]; - this.serialOutBuffer += c; - if (c == "\n") { - Runtime.postMessage({ - type: "serial", - data: this.serialOutBuffer, - id: runtime.id, - sim: true - }) - this.serialOutBuffer = "" - break; - } - } - } - } - - export class Image extends RefObject { - public static height: number = 5; - public width: number; - public data: number[]; - constructor(width: number, data: number[]) { - super() - this.width = width; - this.data = data; - } - - public print() { - console.log(`Image id:${this.id} refs:${this.refcnt} size:${this.width}x${Image.height}`) - } - public get(x: number, y: number): number { - if (x < 0 || x >= this.width || y < 0 || y >= 5) return 0; - return this.data[y * this.width + x]; - } - public set(x: number, y: number, v: number) { - if (x < 0 || x >= this.width || y < 0 || y >= 5) return; - this.data[y * this.width + x] = Math.max(0, Math.min(255, 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 shiftLeft(cols: number) { - for (let x = 0; x < this.width; ++x) - for (let y = 0; y < 5; ++y) - this.set(x, y, x < this.width - cols ? this.get(x + cols, y) : 0); - } - - public shiftRight(cols: number) { - for (let x = this.width - 1; x <= 0; --x) - for (let y = 0; y < 5; ++y) - this.set(x, y, x > cols ? this.get(x - cols, y) : 0); - } - - public clear(): void { - for (let i = 0; i < this.data.length; ++i) - this.data[i] = 0; - } - } - - export function createInternalImage(width: number): Image { - let img = createImage(width) - pxsim.noLeakTracking(img) - return img - } - - 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 = createInternalImage(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 { - const 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; - let font = createInternalImage(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/state/accelerometer.ts b/sim/state/accelerometer.ts new file mode 100644 index 00000000..dc57b1f1 --- /dev/null +++ b/sim/state/accelerometer.ts @@ -0,0 +1,389 @@ +namespace pxsim.input { + export function onGesture(gesture: number, handler: RefAction) { + let b = board().accelerometerState; + b.accelerometer.activate(); + + if (gesture == 11 && !b.useShake) { // SAKE + b.useShake = true; + runtime.queueDisplayUpdate(); + } + pxt.registerWithDal(DAL.MICROBIT_ID_GESTURE, gesture, handler); + } + + export function acceleration(dimension: number): number { + let b = board().accelerometerState; + let acc = b.accelerometer; + acc.activate(); + switch (dimension) { + case 0: return acc.getX(); + case 1: return acc.getY(); + case 2: return acc.getZ(); + default: return Math.floor(Math.sqrt(acc.instantaneousAccelerationSquared())); + } + } + + export function rotation(kind: number): number { + let b = board().accelerometerState; + let acc = b.accelerometer; + acc.activate(); + let x = acc.getX(MicroBitCoordinateSystem.NORTH_EAST_DOWN); + let y = acc.getX(MicroBitCoordinateSystem.NORTH_EAST_DOWN); + let z = acc.getX(MicroBitCoordinateSystem.NORTH_EAST_DOWN); + + let roll = Math.atan2(y, z); + let pitch = Math.atan(-x / (y * Math.sin(roll) + z * Math.cos(roll))); + + let r = 0; + switch (kind) { + case 0: r = pitch; break; + case 1: r = roll; break; + } + return Math.floor(r / Math.PI * 180); + } + + export function setAccelerometerRange(range: number) { + let b = board().accelerometerState; + b.accelerometer.setSampleRange(range); + } +} + +namespace pxsim { + interface AccelerometerSample { + x: number; + y: number; + z: number; + } + + interface ShakeHistory { + x: boolean; + y: boolean; + z: boolean; + count: number; + shaken: number; + timer: number; + } + + /** + * Co-ordinate systems that can be used. + * RAW: Unaltered data. Data will be returned directly from the accelerometer. + * + * SIMPLE_CARTESIAN: Data will be returned based on an easy to understand alignment, consistent with the cartesian system taught in schools. + * When held upright, facing the user: + * + * / + * +--------------------+ z + * | | + * | ..... | + * | * ..... * | + * ^ | ..... | + * | | | + * y +--------------------+ x--> + * + * + * NORTH_EAST_DOWN: Data will be returned based on the industry convention of the North East Down (NED) system. + * When held upright, facing the user: + * + * z + * +--------------------+ / + * | | + * | ..... | + * | * ..... * | + * ^ | ..... | + * | | | + * x +--------------------+ y--> + * + */ + export enum MicroBitCoordinateSystem { + RAW, + SIMPLE_CARTESIAN, + NORTH_EAST_DOWN + } + + export class Accelerometer { + private sigma: number = 0; // the number of ticks that the instantaneous gesture has been stable. + private lastGesture: number = 0; // the last, stable gesture recorded. + private currentGesture: number = 0 // the instantaneous, unfiltered gesture detected. + private sample: AccelerometerSample = { x: 0, y: 0, z: -1023 } + private shake: ShakeHistory = { x: false, y: false, z: false, count: 0, shaken: 0, timer: 0 }; // State information needed to detect shake events. + private pitch: number; + private roll: number; + private id: number; + public isActive = false; + public sampleRange = 2; + + constructor(public runtime: Runtime) { + this.id = DAL.MICROBIT_ID_ACCELEROMETER; + } + + public setSampleRange(range: number) { + this.activate(); + this.sampleRange = Math.max(1, Math.min(8, range)); + } + + public activate() { + if (!this.isActive) { + this.isActive = true; + this.runtime.queueDisplayUpdate(); + } + } + + /** + * Reads the acceleration data from the accelerometer, and stores it in our buffer. + * This is called by the tick() member function, if the interrupt is set! + */ + public update(x: number, y: number, z: number) { + // read MSB values... + this.sample.x = Math.floor(x); + this.sample.y = Math.floor(y); + this.sample.z = Math.floor(z); + + // Update gesture tracking + this.updateGesture(); + + // Indicate that a new sample is available + board().bus.queue(this.id, DAL.MICROBIT_ACCELEROMETER_EVT_DATA_UPDATE) + } + + public instantaneousAccelerationSquared() { + // Use pythagoras theorem to determine the combined force acting on the device. + return this.sample.x * this.sample.x + this.sample.y * this.sample.y + this.sample.z * this.sample.z; + } + + /** + * Service function. Determines the best guess posture of the device based on instantaneous data. + * This makes no use of historic data (except for shake), and forms this input to the filter implemented in updateGesture(). + * + * @return A best guess of the current posture of the device, based on instantaneous data. + */ + private instantaneousPosture(): number { + let force = this.instantaneousAccelerationSquared(); + let shakeDetected = false; + + // Test for shake events. + // We detect a shake by measuring zero crossings in each axis. In other words, if we see a strong acceleration to the left followed by + // a string acceleration to the right, then we can infer a shake. Similarly, we can do this for each acxis (left/right, up/down, in/out). + // + // If we see enough zero crossings in succession (MICROBIT_ACCELEROMETER_SHAKE_COUNT_THRESHOLD), then we decide that the device + // has been shaken. + if ((this.getX() < -DAL.MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && this.shake.x) || (this.getX() > DAL.MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && !this.shake.x)) { + shakeDetected = true; + this.shake.x = !this.shake.x; + } + + if ((this.getY() < -DAL.MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && this.shake.y) || (this.getY() > DAL.MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && !this.shake.y)) { + shakeDetected = true; + this.shake.y = !this.shake.y; + } + + if ((this.getZ() < -DAL.MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && this.shake.z) || (this.getZ() > DAL.MICROBIT_ACCELEROMETER_SHAKE_TOLERANCE && !this.shake.z)) { + shakeDetected = true; + this.shake.z = !this.shake.z; + } + + if (shakeDetected && this.shake.count < DAL.MICROBIT_ACCELEROMETER_SHAKE_COUNT_THRESHOLD && ++this.shake.count == DAL.MICROBIT_ACCELEROMETER_SHAKE_COUNT_THRESHOLD) + this.shake.shaken = 1; + + if (++this.shake.timer >= DAL.MICROBIT_ACCELEROMETER_SHAKE_DAMPING) { + this.shake.timer = 0; + if (this.shake.count > 0) { + if (--this.shake.count == 0) + this.shake.shaken = 0; + } + } + + if (this.shake.shaken) + return DAL.MICROBIT_ACCELEROMETER_EVT_SHAKE; + + let sq = (n: number) => n * n + + if (force < sq(DAL.MICROBIT_ACCELEROMETER_FREEFALL_TOLERANCE)) + return DAL.MICROBIT_ACCELEROMETER_EVT_FREEFALL; + + if (force > sq(DAL.MICROBIT_ACCELEROMETER_3G_TOLERANCE)) + return DAL.MICROBIT_ACCELEROMETER_EVT_3G; + + if (force > sq(DAL.MICROBIT_ACCELEROMETER_6G_TOLERANCE)) + return DAL.MICROBIT_ACCELEROMETER_EVT_6G; + + if (force > sq(DAL.MICROBIT_ACCELEROMETER_8G_TOLERANCE)) + return DAL.MICROBIT_ACCELEROMETER_EVT_8G; + + // Determine our posture. + if (this.getX() < (-1000 + DAL.MICROBIT_ACCELEROMETER_TILT_TOLERANCE)) + return DAL.MICROBIT_ACCELEROMETER_EVT_TILT_LEFT; + + if (this.getX() > (1000 - DAL.MICROBIT_ACCELEROMETER_TILT_TOLERANCE)) + return DAL.MICROBIT_ACCELEROMETER_EVT_TILT_RIGHT; + + if (this.getY() < (-1000 + DAL.MICROBIT_ACCELEROMETER_TILT_TOLERANCE)) + return DAL.MICROBIT_ACCELEROMETER_EVT_TILT_DOWN; + + if (this.getY() > (1000 - DAL.MICROBIT_ACCELEROMETER_TILT_TOLERANCE)) + return DAL.MICROBIT_ACCELEROMETER_EVT_TILT_UP; + + if (this.getZ() < (-1000 + DAL.MICROBIT_ACCELEROMETER_TILT_TOLERANCE)) + return DAL.MICROBIT_ACCELEROMETER_EVT_FACE_UP; + + if (this.getZ() > (1000 - DAL.MICROBIT_ACCELEROMETER_TILT_TOLERANCE)) + return DAL.MICROBIT_ACCELEROMETER_EVT_FACE_DOWN; + + return 0; + } + + updateGesture() { + // Determine what it looks like we're doing based on the latest sample... + let g = this.instantaneousPosture(); + + // Perform some low pass filtering to reduce jitter from any detected effects + if (g == this.currentGesture) { + if (this.sigma < DAL.MICROBIT_ACCELEROMETER_GESTURE_DAMPING) + this.sigma++; + } + else { + this.currentGesture = g; + this.sigma = 0; + } + + // If we've reached threshold, update our record and raise the relevant event... + if (this.currentGesture != this.lastGesture && this.sigma >= DAL.MICROBIT_ACCELEROMETER_GESTURE_DAMPING) { + this.lastGesture = this.currentGesture; + board().bus.queue(DAL.MICROBIT_ID_GESTURE, this.lastGesture); + } + } + + /** + * Reads the X axis value of the latest update from the accelerometer. + * @param system The coordinate system to use. By default, a simple cartesian system is provided. + * @return The force measured in the X axis, in milli-g. + * + * Example: + * @code + * uBit.accelerometer.getX(); + * uBit.accelerometer.getX(RAW); + * @endcode + */ + public getX(system: MicroBitCoordinateSystem = MicroBitCoordinateSystem.SIMPLE_CARTESIAN): number { + this.activate(); + switch (system) { + case MicroBitCoordinateSystem.SIMPLE_CARTESIAN: + return -this.sample.x; + + case MicroBitCoordinateSystem.NORTH_EAST_DOWN: + return this.sample.y; + //case MicroBitCoordinateSystem.SIMPLE_CARTESIAN.RAW: + default: + return this.sample.x; + } + } + + /** + * Reads the Y axis value of the latest update from the accelerometer. + * @param system The coordinate system to use. By default, a simple cartesian system is provided. + * @return The force measured in the Y axis, in milli-g. + * + * Example: + * @code + * uBit.accelerometer.getY(); + * uBit.accelerometer.getY(RAW); + * @endcode + */ + public getY(system: MicroBitCoordinateSystem = MicroBitCoordinateSystem.SIMPLE_CARTESIAN): number { + this.activate(); + switch (system) { + case MicroBitCoordinateSystem.SIMPLE_CARTESIAN: + return -this.sample.y; + + case MicroBitCoordinateSystem.NORTH_EAST_DOWN: + return -this.sample.x; + //case RAW: + default: + return this.sample.y; + } + } + + /** + * Reads the Z axis value of the latest update from the accelerometer. + * @param system The coordinate system to use. By default, a simple cartesian system is provided. + * @return The force measured in the Z axis, in milli-g. + * + * Example: + * @code + * uBit.accelerometer.getZ(); + * uBit.accelerometer.getZ(RAW); + * @endcode + */ + public getZ(system: MicroBitCoordinateSystem = MicroBitCoordinateSystem.SIMPLE_CARTESIAN): number { + this.activate(); + switch (system) { + case MicroBitCoordinateSystem.NORTH_EAST_DOWN: + return -this.sample.z; + //case MicroBitCoordinateSystem.SIMPLE_CARTESIAN: + //case MicroBitCoordinateSystem.RAW: + default: + return this.sample.z; + } + } + + /** + * Provides a rotation compensated pitch of the device, based on the latest update from the accelerometer. + * @return The pitch of the device, in degrees. + * + * Example: + * @code + * uBit.accelerometer.getPitch(); + * @endcode + */ + public getPitch(): number { + this.activate(); + return Math.floor((360 * this.getPitchRadians()) / (2 * Math.PI)); + } + + getPitchRadians(): number { + this.recalculatePitchRoll(); + return this.pitch; + } + + /** + * Provides a rotation compensated roll of the device, based on the latest update from the accelerometer. + * @return The roll of the device, in degrees. + * + * Example: + * @code + * uBit.accelerometer.getRoll(); + * @endcode + */ + public getRoll(): number { + this.activate(); + return Math.floor((360 * this.getRollRadians()) / (2 * Math.PI)); + } + + getRollRadians(): number { + this.recalculatePitchRoll(); + return this.roll; + } + + /** + * Recalculate roll and pitch values for the current sample. + * We only do this at most once per sample, as the necessary trigonemteric functions are rather + * heavyweight for a CPU without a floating point unit... + */ + recalculatePitchRoll() { + let x = this.getX(MicroBitCoordinateSystem.NORTH_EAST_DOWN); + let y = this.getY(MicroBitCoordinateSystem.NORTH_EAST_DOWN); + let z = this.getZ(MicroBitCoordinateSystem.NORTH_EAST_DOWN); + + this.roll = Math.atan2(y, z); + this.pitch = Math.atan(-x / (y * Math.sin(this.roll) + z * Math.cos(this.roll))); + } + + } + + export class AccelerometerState { + accelerometer: Accelerometer; + useShake = false; + + constructor(runtime: Runtime) { + this.accelerometer = new Accelerometer(runtime); + } + } +} \ No newline at end of file diff --git a/sim/state/buttonpair.ts b/sim/state/buttonpair.ts new file mode 100644 index 00000000..fdad2305 --- /dev/null +++ b/sim/state/buttonpair.ts @@ -0,0 +1,41 @@ +namespace pxsim.input { + export function onButtonPressed(button: number, handler: RefAction): void { + let b = board().buttonPairState; + if (button == DAL.MICROBIT_ID_BUTTON_AB && !b.usesButtonAB) { + b.usesButtonAB = true; + runtime.queueDisplayUpdate(); + } + pxt.registerWithDal(button, DAL.MICROBIT_BUTTON_EVT_CLICK, handler); + } + + export function buttonIsPressed(button: number): boolean { + let b = board().buttonPairState; + if (button == DAL.MICROBIT_ID_BUTTON_AB && !b.usesButtonAB) { + b.usesButtonAB = true; + runtime.queueDisplayUpdate(); + } + if (button == DAL.MICROBIT_ID_BUTTON_A) return b.aBtn.pressed; + if (button == DAL.MICROBIT_ID_BUTTON_B) return b.bBtn.pressed; + return b.abBtn.pressed || (b.aBtn.pressed && b.bBtn.pressed); + } +} + +namespace pxsim { + export class Button { + constructor(public id: number) { } + pressed: boolean; + } + + export class ButtonPairState { + usesButtonAB: boolean = false; + aBtn: Button; + bBtn: Button; + abBtn: Button; + + constructor() { + this.aBtn = new Button(DAL.MICROBIT_ID_BUTTON_A); + this.bBtn = new Button(DAL.MICROBIT_ID_BUTTON_B); + this.abBtn = new Button(DAL.MICROBIT_ID_BUTTON_AB); + } + } +} \ No newline at end of file diff --git a/sim/state/compass.ts b/sim/state/compass.ts new file mode 100644 index 00000000..36f3614f --- /dev/null +++ b/sim/state/compass.ts @@ -0,0 +1,22 @@ +namespace pxsim.input { + export function compassHeading(): number { + let b = board().compassState; + if (!b.usesHeading) { + b.usesHeading = true; + runtime.queueDisplayUpdate(); + } + return b.heading; + } + + export function magneticForce(): number { + // TODO + return 0; + } +} + +namespace pxsim { + export class CompassState { + usesHeading = false; + heading = 90; + } +} \ No newline at end of file diff --git a/sim/state/edgeconnector.ts b/sim/state/edgeconnector.ts new file mode 100644 index 00000000..d3278a64 --- /dev/null +++ b/sim/state/edgeconnector.ts @@ -0,0 +1,178 @@ +namespace pxsim.input { + export function onPinPressed(pinId: number, handler: RefAction) { + let pin = getPin(pinId); + if (!pin) return; + pin.isTouched(); + pxt.registerWithDal(pin.id, DAL.MICROBIT_BUTTON_EVT_CLICK, handler); + } + + export function onPinReleased(pinId: number, handler: RefAction) { + let pin = getPin(pinId); + if (!pin) return; + pin.isTouched(); + pxt.registerWithDal(pin.id, DAL.MICROBIT_BUTTON_EVT_UP, handler); + } + + export function pinIsPressed(pinId: number): boolean { + let pin = getPin(pinId); + if (!pin) return false; + return pin.isTouched(); + } +} + +namespace pxsim { + export function getPin(id: number) { + return board().edgeConnectorState.getPin(id); + } + + export enum PinFlags { + Unused = 0, + Digital = 0x0001, + Analog = 0x0002, + Input = 0x0004, + Output = 0x0008, + Touch = 0x0010 + } + + export class Pin { + constructor(public id: number) { } + touched = false; + value = 0; + period = 0; + mode = PinFlags.Unused; + pitch = false; + pull = 0; // PullDown + + isTouched(): boolean { + this.mode = PinFlags.Touch; + return this.touched; + } + } + + export class EdgeConnectorState { + pins: Pin[]; + + constructor() { + this.pins = [ + new Pin(DAL.MICROBIT_ID_IO_P0), + new Pin(DAL.MICROBIT_ID_IO_P1), + new Pin(DAL.MICROBIT_ID_IO_P2), + new Pin(DAL.MICROBIT_ID_IO_P3), + new Pin(DAL.MICROBIT_ID_IO_P4), + new Pin(DAL.MICROBIT_ID_IO_P5), + new Pin(DAL.MICROBIT_ID_IO_P6), + new Pin(DAL.MICROBIT_ID_IO_P7), + new Pin(DAL.MICROBIT_ID_IO_P8), + new Pin(DAL.MICROBIT_ID_IO_P9), + new Pin(DAL.MICROBIT_ID_IO_P10), + new Pin(DAL.MICROBIT_ID_IO_P11), + new Pin(DAL.MICROBIT_ID_IO_P12), + new Pin(DAL.MICROBIT_ID_IO_P13), + new Pin(DAL.MICROBIT_ID_IO_P14), + new Pin(DAL.MICROBIT_ID_IO_P15), + new Pin(DAL.MICROBIT_ID_IO_P16), + null, + null, + new Pin(DAL.MICROBIT_ID_IO_P19), + new Pin(DAL.MICROBIT_ID_IO_P20) + ]; + } + + public getPin(id: number) { + return this.pins.filter(p => p && p.id == id)[0] || null + } + } +} + +namespace pxsim.pins { + export function digitalReadPin(pinId: number): number { + let pin = getPin(pinId); + if (!pin) return; + pin.mode = PinFlags.Digital | PinFlags.Input; + return pin.value > 100 ? 1 : 0; + } + + export function digitalWritePin(pinId: number, value: number) { + let pin = getPin(pinId); + if (!pin) return; + pin.mode = PinFlags.Digital | PinFlags.Output; + pin.value = value > 0 ? 1023 : 0; + runtime.queueDisplayUpdate(); + } + + export function setPull(pinId: number, pull: number) { + let pin = getPin(pinId); + if (!pin) return; + pin.pull = pull; + } + + export function analogReadPin(pinId: number): number { + let pin = getPin(pinId); + if (!pin) return; + pin.mode = PinFlags.Analog | PinFlags.Input; + return pin.value || 0; + } + + export function analogWritePin(pinId: number, value: number) { + let pin = getPin(pinId); + if (!pin) return; + pin.mode = PinFlags.Analog | PinFlags.Output; + pin.value = value ? 1 : 0; + runtime.queueDisplayUpdate(); + } + + export function analogSetPeriod(pinId: number, micros: number) { + let pin = getPin(pinId); + if (!pin) return; + pin.mode = PinFlags.Analog | PinFlags.Output; + pin.period = micros; + runtime.queueDisplayUpdate(); + } + + export function servoWritePin(pinId: number, value: number) { + analogSetPeriod(pinId, 20000); + // TODO + } + + export function servoSetPulse(pinId: number, micros: number) { + let pin = getPin(pinId); + if (!pin) return; + // TODO + } + + export function analogSetPitchPin(pinId: number) { + let pin = getPin(pinId); + if (!pin) return; + board().edgeConnectorState.pins.filter(p => !!p).forEach(p => p.pitch = false); + pin.pitch = true; + } + + export function analogPitch(frequency: number, ms: number) { + // update analog output + let pins = board().edgeConnectorState.pins; + let pin = pins.filter(pin => !!pin && pin.pitch)[0] || pins[0]; + pin.mode = PinFlags.Analog | PinFlags.Output; + if (frequency <= 0) { + pin.value = 0; + pin.period = 0; + } else { + pin.value = 512; + pin.period = 1000000 / frequency; + } + runtime.queueDisplayUpdate(); + + let cb = getResume(); + AudioContextManager.tone(frequency, 1); + if (ms <= 0) cb(); + else { + setTimeout(() => { + AudioContextManager.stop(); + pin.value = 0; + pin.period = 0; + pin.mode = PinFlags.Unused; + runtime.queueDisplayUpdate(); + cb() + }, ms); + } + } +} \ No newline at end of file diff --git a/sim/state/ledmatrix.ts b/sim/state/ledmatrix.ts new file mode 100644 index 00000000..ee02c41b --- /dev/null +++ b/sim/state/ledmatrix.ts @@ -0,0 +1,357 @@ +namespace pxsim { + export enum DisplayMode { + bw, + greyscale + } + + export class LedMatrixState { + image = createInternalImage(5); + brigthness = 255; + displayMode = DisplayMode.bw; + font: Image = createFont(); + + animationQ: AnimationQueue; + + constructor(runtime: Runtime) { + this.animationQ = new AnimationQueue(runtime); + } + } + + export class Image extends RefObject { + public static height: number = 5; + public width: number; + public data: number[]; + constructor(width: number, data: number[]) { + super(); + this.width = width; + this.data = data; + } + public print() { + console.log(`Image id:${this.id} refs:${this.refcnt} size:${this.width}x${Image.height}`) + } + public get(x: number, y: number): number { + if (x < 0 || x >= this.width || y < 0 || y >= 5) return 0; + return this.data[y * this.width + x]; + } + public set(x: number, y: number, v: number) { + if (x < 0 || x >= this.width || y < 0 || y >= 5) return; + this.data[y * this.width + x] = Math.max(0, Math.min(255, 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 shiftLeft(cols: number) { + for (let x = 0; x < this.width; ++x) + for (let y = 0; y < 5; ++y) + this.set(x, y, x < this.width - cols ? this.get(x + cols, y) : 0); + } + + public shiftRight(cols: number) { + for (let x = this.width - 1; x <= 0; --x) + for (let y = 0; y < 5; ++y) + this.set(x, y, x > cols ? this.get(x - cols, y) : 0); + } + + public clear(): void { + for (let i = 0; i < this.data.length; ++i) + this.data[i] = 0; + } + } + + export function createInternalImage(width: number): Image { + let img = createImage(width) + pxsim.noLeakTracking(img) + return img + } + + 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().ledMatrixState.font; + let w = font.width; + let sprite = createInternalImage(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 { + const 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; + let font = createInternalImage(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; + } + + 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) + }) + } + } +} + +namespace pxsim.images { + export function createImage(img: Image) { + return img + } + export function createBigImage(img: Image) { + return img + } +} + +namespace pxsim.ImageMethods { + export function showImage(leds: Image, offset: number) { + if (!leds) panic(PanicCode.MICROBIT_NULL_DEREFERENCE); + + leds.copyTo(offset, 5, board().ledMatrixState.image, 0) + runtime.queueDisplayUpdate() + } + + export function plotImage(leds: Image, offset: number): void { + if (!leds) panic(PanicCode.MICROBIT_NULL_DEREFERENCE); + + leds.copyTo(offset, 5, board().ledMatrixState.image, 0) + runtime.queueDisplayUpdate() + } + + export function height(leds: Image): number { + if (!leds) panic(PanicCode.MICROBIT_NULL_DEREFERENCE); + return Image.height; + } + + export function width(leds: Image): number { + if (!leds) panic(PanicCode.MICROBIT_NULL_DEREFERENCE); + return leds.width; + } + + export function plotFrame(leds: Image, frame: number) { + ImageMethods.plotImage(leds, frame * Image.height); + } + + export function showFrame(leds: Image, frame: number) { + ImageMethods.showImage(leds, frame * Image.height); + } + + export function pixel(leds: Image, x: number, y: number): number { + if (!leds) panic(PanicCode.MICROBIT_NULL_DEREFERENCE); + return leds.get(x, y); + } + + export function setPixel(leds: Image, x: number, y: number, v: number) { + if (!leds) panic(PanicCode.MICROBIT_NULL_DEREFERENCE); + leds.set(x, y, v); + } + + export function clear(leds: Image) { + if (!leds) panic(PanicCode.MICROBIT_NULL_DEREFERENCE); + + leds.clear(); + } + + export function setPixelBrightness(i: Image, x: number, y: number, b: number) { + if (!i) panic(PanicCode.MICROBIT_NULL_DEREFERENCE); + + i.set(x, y, b); + } + + export function pixelBrightness(i: Image, x: number, y: number): number { + if (!i) panic(PanicCode.MICROBIT_NULL_DEREFERENCE); + + return i.get(x, y); + } + + export function scrollImage(leds: Image, stride: number, interval: number): void { + if (!leds) panic(PanicCode.MICROBIT_NULL_DEREFERENCE); + if (stride == 0) stride = 1; + + let cb = getResume(); + let off = stride > 0 ? 0 : leds.width - 1; + let display = board().ledMatrixState.image; + + board().ledMatrixState.animationQ.enqueue({ + interval: interval, + frame: () => { + //TODO: support right to left. + if (off >= leds.width || off < 0) return false; + stride > 0 ? display.shiftLeft(stride) : display.shiftRight(-stride); + let c = Math.min(stride, leds.width - off); + leds.copyTo(off, c, display, 5 - stride) + off += stride; + return true; + }, + whenDone: cb + }) + } +} + +namespace pxsim.basic { + export function showNumber(x: number, interval: number) { + if (interval < 0) return; + + let leds = createImageFromString(x.toString()); + if (x < 0 || x >= 10) ImageMethods.scrollImage(leds, 1, interval); + else showLeds(leds, interval * 5); + } + + export function showString(s: string, interval: number) { + if (interval < 0) return; + if (s.length == 0) { + clearScreen(); + pause(interval * 5); + } else { + if (s.length == 1) showLeds(createImageFromString(s + " "), interval * 5) + else ImageMethods.scrollImage(createImageFromString(s + " "), 1, interval); + } + } + + export function showLeds(leds: Image, delay: number): void { + showAnimation(leds, delay); + } + + export function clearScreen() { + board().ledMatrixState.image.clear(); + runtime.queueDisplayUpdate() + } + + export function showAnimation(leds: Image, interval: number): void { + ImageMethods.scrollImage(leds, 5, interval); + } + + export function plotLeds(leds: Image): void { + ImageMethods.plotImage(leds, 0); + } +} + +namespace pxsim.led { + export function plot(x: number, y: number) { + board().ledMatrixState.image.set(x, y, 255); + runtime.queueDisplayUpdate() + } + + export function unplot(x: number, y: number) { + board().ledMatrixState.image.set(x, y, 0); + runtime.queueDisplayUpdate() + } + + export function point(x: number, y: number): boolean { + return !!board().ledMatrixState.image.get(x, y); + } + + export function brightness(): number { + return board().ledMatrixState.brigthness; + } + + export function setBrightness(value: number): void { + board().ledMatrixState.brigthness = value; + runtime.queueDisplayUpdate() + } + + export function stopAnimation(): void { + board().ledMatrixState.animationQ.cancelAll(); + } + + export function setDisplayMode(mode: DisplayMode): void { + board().ledMatrixState.displayMode = mode; + runtime.queueDisplayUpdate() + } + + export function screenshot(): Image { + let img = createImage(5) + board().ledMatrixState.image.copyTo(0, 5, img, 0); + return img; + } +} \ No newline at end of file diff --git a/sim/state/lightsensor.ts b/sim/state/lightsensor.ts new file mode 100644 index 00000000..5df56147 --- /dev/null +++ b/sim/state/lightsensor.ts @@ -0,0 +1,17 @@ +namespace pxsim { + export class LightSensorState { + usesLightLevel = false; + lightLevel = 128; + } +} + +namespace pxsim.input { + export function lightLevel(): number { + let b = board().lightSensorState; + if (!b.usesLightLevel) { + b.usesLightLevel = true; + runtime.queueDisplayUpdate(); + } + return b.lightLevel; + } +} \ No newline at end of file diff --git a/sim/state/misc.ts b/sim/state/misc.ts new file mode 100644 index 00000000..2fd7ae95 --- /dev/null +++ b/sim/state/misc.ts @@ -0,0 +1,228 @@ +namespace pxsim { + /** + * Error codes used in the micro:bit runtime. + */ + export enum PanicCode { + // PANIC Codes. These are not return codes, but are terminal conditions. + // These induce a panic operation, where all code stops executing, and a panic state is + // entered where the panic code is diplayed. + + // Out out memory error. Heap storage was requested, but is not available. + MICROBIT_OOM = 20, + + // Corruption detected in the micro:bit heap space + MICROBIT_HEAP_ERROR = 30, + + // Dereference of a NULL pointer through the ManagedType class, + MICROBIT_NULL_DEREFERENCE = 40, + }; + + export function panic(code: number) { + console.log("PANIC:", code) + led.setBrightness(255); + let img = board().ledMatrixState.image; + img.clear(); + img.set(0, 4, 255); + img.set(1, 3, 255); + img.set(2, 3, 255); + img.set(3, 3, 255); + img.set(4, 4, 255); + img.set(0, 0, 255); + img.set(1, 0, 255); + img.set(0, 1, 255); + img.set(1, 1, 255); + img.set(3, 0, 255); + img.set(4, 0, 255); + img.set(3, 1, 255); + img.set(4, 1, 255); + runtime.updateDisplay(); + + throw new Error("PANIC " + code) + } + + export namespace AudioContextManager { + let _context: any; // AudioContext + let _vco: any; // OscillatorNode; + let _vca: any; // GainNode; + + function context(): any { + if (!_context) _context = freshContext(); + return _context; + } + + function freshContext(): any { + (window).AudioContext = (window).AudioContext || (window).webkitAudioContext; + if ((window).AudioContext) { + try { + // this call my crash. + // SyntaxError: audio resources unavailable for AudioContext construction + return new (window).AudioContext(); + } catch (e) { } + } + return undefined; + } + + export function stop() { + if (_vca) _vca.gain.value = 0; + } + + export function tone(frequency: number, gain: number) { + if (frequency <= 0) return; + let ctx = context(); + if (!ctx) return; + + gain = Math.max(0, Math.min(1, gain)); + if (!_vco) { + try { + _vco = ctx.createOscillator(); + _vca = ctx.createGain(); + _vco.connect(_vca); + _vca.connect(ctx.destination); + _vca.gain.value = gain; + _vco.start(0); + } catch (e) { + _vco = undefined; + _vca = undefined; + return; + } + } + + _vco.frequency.value = frequency; + _vca.gain.value = gain; + } + } + + export interface RuntimeOptions { + theme: string; + } + + 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); + } + } +} + +namespace pxsim.basic { + export var pause = thread.pause; + export var forever = thread.forever; +} + +namespace pxsim.control { + export var inBackground = thread.runInBackground; + + export function reset() { + U.userError("reset not implemented in simulator yet") + } + + export function waitMicros(micros: number) { + // TODO + } + + export function deviceName(): string { + let b = board(); + return b && b.id + ? b.id.slice(0, 4) + : "abcd"; + } + + export function deviceSerialNumber(): number { + let b = board(); + return parseInt(b && b.id + ? b.id.slice(1) + : "42"); + } + + export function onEvent(id: number, evid: number, handler: RefAction) { + pxt.registerWithDal(id, evid, handler) + } + + export function raiseEvent(id: number, evid: number, mode: number) { + // TODO mode? + board().bus.queue(id, evid) + } +} + +namespace pxsim.pxt { + export function registerWithDal(id: number, evid: number, handler: RefAction) { + board().bus.listen(id, evid, handler); + } +} + +namespace pxsim.input { + export function runningTime(): number { + return runtime.runningTime(); + } + + export function calibrate() { + } +} + +namespace pxsim.pins { + export function onPulsed(name: number, pulse: number, body: RefAction) { + } + + export function pulseDuration(): number { + return 0; + } + + export function createBuffer(sz: number) { + return pxsim.BufferMethods.createBuffer(sz) + } + + export function pulseIn(name: number, value: number, maxDuration: number): number { + let pin = getPin(name); + if (!pin) return 0; + + return 5000; + } + + 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 + } +} + +namespace pxsim.bluetooth { + export function startIOPinService(): void { + // TODO + } + export function startLEDService(): void { + // TODO + } + export function startTemperatureService(): void { + // TODO + } + export function startMagnetometerService(): void { + // TODO + } + export function startAccelerometerService(): void { + // TODO + } + export function startButtonService(): void { + // TODO + } +} + diff --git a/sim/state/neopixel.ts b/sim/state/neopixel.ts new file mode 100644 index 00000000..0a18d60b --- /dev/null +++ b/sim/state/neopixel.ts @@ -0,0 +1,57 @@ +namespace pxsim { + export function sendBufferAsm(buffer: Buffer, pin: DigitalPin) { + let b = board(); + if (b) { + let np = b.neopixelState; + if (np) { + np.updateBuffer(buffer, pin); + runtime.queueDisplayUpdate(); + } + } + } +} + +namespace pxsim { + export enum NeoPixelMode {RGB, RGBW}; + export type RGBW = [number, number, number, number]; + + function readNeoPixelBuffer(inBuffer: Uint8Array[], outColors: RGBW[], mode: NeoPixelMode) { + let buf = inBuffer; + let stride = mode === NeoPixelMode.RGBW ? 4 : 3; + let pixelCount = Math.floor(buf.length / stride); + for (let i = 0; i < pixelCount; i++) { + // NOTE: for whatever reason, NeoPixels pack GRB not RGB + let r = buf[i * stride + 1] as any as number + let g = buf[i * stride + 0] as any as number + let b = buf[i * stride + 2] as any as number + let w = 0; + if (stride === 4) + w = buf[i * stride + 3] as any as number + outColors[i] = [r, g, b, w] + } + + } + + export class NeoPixelState { + private buffers: {[pin: number]: Uint8Array[]} = {}; + private colors: {[pin: number]: RGBW[]} = {}; + private dirty: {[pin: number]: boolean} = {}; + + public updateBuffer(buffer: Buffer, pin: DigitalPin) { + //update buffers + let buf = (buffer).data; + this.buffers[pin] = buf; + this.dirty[pin] = true; + } + + public getColors(pin: number, mode: NeoPixelMode): RGBW[] { + let outColors = this.colors[pin] || (this.colors[pin] = []); + if (this.dirty[pin]) { + let buf = this.buffers[pin] || (this.buffers[pin] = []); + readNeoPixelBuffer(buf, outColors, mode); + this.dirty[pin] = false; + } + return outColors; + } + } +} \ No newline at end of file diff --git a/sim/state/radio.ts b/sim/state/radio.ts new file mode 100644 index 00000000..7a48ea3c --- /dev/null +++ b/sim/state/radio.ts @@ -0,0 +1,158 @@ +namespace pxsim { + export interface PacketBuffer { + data: number[] | string; + 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 < 4) { + this.datagram.push(packet); + } + (runtime.board).bus.queue(DAL.MICROBIT_ID_RADIO, DAL.MICROBIT_RADIO_EVT_DATAGRAM); + } + + send(buffer: number[] | string) { + if (buffer instanceof String) buffer = buffer.slice(0, 32); + else buffer = buffer.slice(0, 8); + + Runtime.postMessage({ + type: "radiopacket", + data: buffer + }) + } + + recv(): PacketBuffer { + let 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 + power = 0; + transmitSerialNumber = false; + datagram: RadioDatagram; + + constructor(private runtime: Runtime) { + this.datagram = new RadioDatagram(runtime); + } + + setGroup(id: number) { + this.groupId = id & 0xff; // byte only + } + + setTransmitPower(power: number) { + this.power = Math.max(0, Math.min(7, power)); + } + + setTransmitSerialNumber(sn: boolean) { + this.transmitSerialNumber = !!sn; + } + + broadcast(msg: number) { + Runtime.postMessage({ + type: "eventbus", + id: DAL.MES_BROADCAST_GENERAL_ID, + eventid: msg, + power: this.power, + group: this.groupId + }) + } + } + + export class RadioState { + bus: RadioBus; + + constructor(runtime: Runtime) { + this.bus = new RadioBus(runtime); + } + + public recievePacket(packet: SimulatorRadioPacketMessage) { + this.bus.datagram.queue({ data: packet.data, rssi: packet.rssi || 0 }) + } + } +} + +namespace pxsim.radio { + export function broadcastMessage(msg: number): void { + board().radioState.bus.broadcast(msg); + } + + export function onBroadcastMessageReceived(msg: number, handler: RefAction): void { + pxt.registerWithDal(DAL.MES_BROADCAST_GENERAL_ID, msg, handler); + } + + export function setGroup(id: number): void { + board().radioState.bus.setGroup(id); + } + + export function setTransmitPower(power: number): void { + board().radioState.bus.setTransmitPower(power); + } + + export function setTransmitSerialNumber(transmit: boolean): void { + board().radioState.bus.setTransmitSerialNumber(transmit); + } + + export function sendNumber(value: number): void { + board().radioState.bus.datagram.send([value]); + } + + export function sendString(msg: string): void { + board().radioState.bus.datagram.send(msg); + } + + export function writeValueToSerial(): void { + let b = board(); + let v = b.radioState.bus.datagram.recv().data[0]; + b.writeSerial(`{v:${v}}`); + } + + export function sendValue(name: string, value: number) { + board().radioState.bus.datagram.send([value]); + } + + export function receiveNumber(): number { + let buffer = board().radioState.bus.datagram.recv().data; + if (buffer instanceof Array) return buffer[0]; + + return 0; + } + + export function receiveString(): string { + let buffer = board().radioState.bus.datagram.recv().data; + if (typeof buffer === "string") return buffer; + return ""; + } + + export function receivedNumberAt(index: number): number { + let buffer = board().radioState.bus.datagram.recv().data; + if (buffer instanceof Array) return buffer[index] || 0; + + return 0; + } + + export function receivedSignalStrength(): number { + return board().radioState.bus.datagram.lastReceived.rssi; + } + + export function onDataReceived(handler: RefAction): void { + pxt.registerWithDal(DAL.MICROBIT_ID_RADIO, DAL.MICROBIT_RADIO_EVT_DATAGRAM, handler); + radio.receiveNumber(); + } +} \ No newline at end of file diff --git a/sim/state/serial.ts b/sim/state/serial.ts new file mode 100644 index 00000000..ff4f9420 --- /dev/null +++ b/sim/state/serial.ts @@ -0,0 +1,54 @@ +namespace pxsim { + export class SerialState { + serialIn: string[] = []; + + public recieveData(data: string) { + this.serialIn.push(); + } + + 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]; + this.serialOutBuffer += c; + if (c == "\n") { + Runtime.postMessage({ + type: "serial", + data: this.serialOutBuffer, + id: runtime.id + }) + this.serialOutBuffer = "" + break; + } + } + } + } +} + +namespace pxsim.serial { + export function writeString(s: string) { + board().writeSerial(s); + } + + export function readString(): string { + return board().serialState.readSerial(); + } + + export function readLine(): string { + return board().serialState.readSerial(); + } + + export function onDataReceived(delimiters: string, handler: RefAction) { + let b = board(); + b.bus.listen(DAL.MICROBIT_ID_SERIAL, DAL.MICROBIT_SERIAL_EVT_DELIM_MATCH, handler); + } + + export function redirect(tx: number, rx: number, rate: number) { + // TODO? + } +} \ No newline at end of file diff --git a/sim/state/thermometer.ts b/sim/state/thermometer.ts new file mode 100644 index 00000000..2e2d1a66 --- /dev/null +++ b/sim/state/thermometer.ts @@ -0,0 +1,18 @@ +namespace pxsim { + export class ThermometerState { + usesTemperature = false; + temperature = 21; + + } +} + +namespace pxsim.input { + export function temperature(): number { + let b = board(); + if (!b.thermometerState.usesTemperature) { + b.thermometerState.usesTemperature = true; + runtime.queueDisplayUpdate(); + } + return b.thermometerState.temperature; + } +} \ No newline at end of file