diff --git a/docs/static/hardware/.gitignore b/docs/static/hardware/.gitignore new file mode 100644 index 00000000..09ade3ad --- /dev/null +++ b/docs/static/hardware/.gitignore @@ -0,0 +1,4 @@ +# don't check in until OSS request is approved +sparkfun-* +raspberrypi-* +arduino-* \ No newline at end of file diff --git a/docs/static/hardware/neopixel.svg b/docs/static/hardware/neopixel.svg new file mode 100644 index 00000000..2d4b81a4 --- /dev/null +++ b/docs/static/hardware/neopixel.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/static/hardware/speaker.svg b/docs/static/hardware/speaker.svg new file mode 100644 index 00000000..6a03638b --- /dev/null +++ b/docs/static/hardware/speaker.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/microbit/music.ts b/libs/microbit/music.ts index dad35efd..75709fa5 100644 --- a/libs/microbit/music.ts +++ b/libs/microbit/music.ts @@ -1,63 +1,110 @@ enum Note { C = 262, //% block=C# + //% blockIdentity=music.noteFrequency CSharp = 277, + //% blockIdentity=music.noteFrequency D = 294, + //% blockIdentity=music.noteFrequency Eb = 311, + //% blockIdentity=music.noteFrequency E = 330, + //% blockIdentity=music.noteFrequency F = 349, //% block=F# + //% blockIdentity=music.noteFrequency FSharp = 370, + //% blockIdentity=music.noteFrequency G = 392, //% block=G# + //% blockIdentity=music.noteFrequency GSharp = 415, + //% blockIdentity=music.noteFrequency A = 440, + //% blockIdentity=music.noteFrequency Bb = 466, + //% blockIdentity=music.noteFrequency B = 494, + //% blockIdentity=music.noteFrequency C3 = 131, //% block=C#3 + //% blockIdentity=music.noteFrequency CSharp3 = 139, + //% blockIdentity=music.noteFrequency D3 = 147, + //% blockIdentity=music.noteFrequency Eb3 = 156, + //% blockIdentity=music.noteFrequency E3 = 165, + //% blockIdentity=music.noteFrequency F3 = 175, //% block=F#3 + //% blockIdentity=music.noteFrequency FSharp3 = 185, + //% blockIdentity=music.noteFrequency G3 = 196, //% block=G#3 + //% blockIdentity=music.noteFrequency GSharp3 = 208, + //% blockIdentity=music.noteFrequency A3 = 220, + //% blockIdentity=music.noteFrequency Bb3 = 233, + //% blockIdentity=music.noteFrequency B3 = 247, + //% blockIdentity=music.noteFrequency C4 = 262, //% block=C#4 + //% blockIdentity=music.noteFrequency CSharp4 = 277, + //% blockIdentity=music.noteFrequency D4 = 294, + //% blockIdentity=music.noteFrequency Eb4 = 311, + //% blockIdentity=music.noteFrequency E4 = 330, + //% blockIdentity=music.noteFrequency F4 = 349, //% block=F#4 + //% blockIdentity=music.noteFrequency FSharp4 = 370, + //% blockIdentity=music.noteFrequency G4 = 392, //% block=G#4 + //% blockIdentity=music.noteFrequency GSharp4 = 415, + //% blockIdentity=music.noteFrequency A4 = 440, + //% blockIdentity=music.noteFrequency Bb4 = 466, + //% blockIdentity=music.noteFrequency B4 = 494, + //% blockIdentity=music.noteFrequency C5 = 523, //% block=C#5 + //% blockIdentity=music.noteFrequency CSharp5 = 555, + //% blockIdentity=music.noteFrequency D5 = 587, + //% blockIdentity=music.noteFrequency Eb5 = 622, + //% blockIdentity=music.noteFrequency E5 = 659, + //% blockIdentity=music.noteFrequency F5 = 698, //% block=F#5 + //% blockIdentity=music.noteFrequency FSharp5 = 740, + //% blockIdentity=music.noteFrequency G5 = 784, //% block=G#5 + //% blockIdentity=music.noteFrequency GSharp5 = 831, + //% blockIdentity=music.noteFrequency A5 = 880, + //% blockIdentity=music.noteFrequency Bb5 = 932, + //% blockIdentity=music.noteFrequency B5 = 989, } @@ -88,6 +135,7 @@ namespace music { */ //% help=music/play-tone weight=90 //% blockId=device_play_note block="play|tone %note=device_note|for %duration=device_beat" icon="\uf025" blockGap=8 + //% parts="speaker" export function playTone(frequency: number, ms: number): void { pins.analogSetPitchPin(AnalogPin.P0); pins.analogPitch(frequency, ms); @@ -99,6 +147,7 @@ namespace music { */ //% help=music/ring-tone weight=80 //% blockId=device_ring block="ring tone (Hz)|%note=device_note" icon="\uf025" blockGap=8 + //% parts="speaker" export function ringTone(frequency: number): void { pins.analogSetPitchPin(AnalogPin.P0); pins.analogPitch(frequency, 0); @@ -110,6 +159,7 @@ namespace music { */ //% help=music/rest weight=79 //% blockId=device_rest block="rest(ms)|%duration=device_beat" + //% parts="speaker" export function rest(ms: number): void { playTone(0, ms); } @@ -121,6 +171,8 @@ namespace music { */ //% weight=50 help=music/note-frequency //% blockId=device_note block="%note" + //% parts="speaker" + //% shim=TD_ID export function noteFrequency(name: Note): number { return name; } @@ -134,6 +186,7 @@ namespace music { */ //% help=music/beat weight=49 //% blockId=device_beat block="%fraction|beat" + //% parts="speaker" export function beat(fraction?: BeatFraction): number { init(); if (fraction == null) fraction = BeatFraction.Whole; @@ -150,6 +203,7 @@ namespace music { */ //% help=music/tempo weight=40 //% blockId=device_tempo block="tempo (bpm)" blockGap=8 + //% parts="speaker" export function tempo(): number { init(); return beatsPerMinute; @@ -161,8 +215,9 @@ namespace music { */ //% help=music/change-tempo weight=39 //% blockId=device_change_tempo block="change tempo by (bpm)|%value" blockGap=8 + //% parts="speaker" export function changeTempoBy(bpm: number): void { - init(); + init(); setTempo(beatsPerMinute + bpm); } @@ -172,6 +227,7 @@ namespace music { */ //% help=music/set-tempo weight=38 //% blockId=device_set_tempo block="set tempo to (bpm)|%value" + //% parts="speaker" export function setTempo(bpm: number): void { init(); if (bpm > 0) { diff --git a/package.json b/package.json index 454ca73c..461a7bac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pxt-microbit", - "version": "0.3.55", + "version": "0.3.63", "description": "micro:bit target for PXT", "keywords": [ "JavaScript", @@ -29,6 +29,6 @@ "typescript": "^1.8.7" }, "dependencies": { - "pxt-core": "0.3.61" + "pxt-core": "0.3.72" } } diff --git a/pxtarget.json b/pxtarget.json index 18f24c81..de3d6af8 100644 --- a/pxtarget.json +++ b/pxtarget.json @@ -75,7 +75,17 @@ }, "simulator": { "autoRun": true, - "aspectRatio": 1.22 + "aspectRatio": 1.22, + "partsAspectRatio": 0.69, + "builtinParts": { + "accelerometer": true, + "buttonpair": true, + "ledmatrix": true, + "speaker": true, + "bluetooth": true, + "thermometer": true, + "compass": true + } }, "compileService": { "yottaTarget": "bbc-microbit-classic-gcc", @@ -136,4 +146,4 @@ "userVoiceApiKey": "WEkkIGaj1WtJnSUF59iwaA", "userVoiceForumId": 402381 } -} +} \ No newline at end of file diff --git a/sim/allocator.ts b/sim/allocator.ts new file mode 100644 index 00000000..de1b0cd4 --- /dev/null +++ b/sim/allocator.ts @@ -0,0 +1,432 @@ + +namespace pxsim { + export interface AllocatorOpts { + boardDef: BoardDefinition, + cmpDefs: Map, + fnArgs: any, + getBBCoord: (loc: BBRowCol) => visuals.Coord, + cmpList: string[] + }; + export interface AllocatorResult { + powerWires: WireInst[], + components: CmpAndWireInst[] + } + + export interface CmpAndWireInst { + component: CmpInst, + wires: WireInst[] + } + export interface CmpInst { + name: string, + breadboardStartColumn: number, + breadboardStartRow: string, + assemblyStep: number, + visual: string | PartVisualDefinition, + microbitPins: string[], + otherArgs?: string[], + } + export interface WireInst { + start: Loc, + end: Loc, + color: string, + assemblyStep: number + }; + interface PartialCmpAlloc { + name: string, + def: PartDefinition, + pinsAssigned: string[], + pinsNeeded: number | number[], + breadboardColumnsNeeded: number, + otherArgs?: string[], + } + + interface AllocLocOpts { + nearestBBPin?: BBRowCol, + startColumn?: number, + cmpGPIOPins?: string[], + }; + interface AllocWireOpts { + startColumn: number, + cmpGPIOPins: string[], + } + interface AllocBlock { + cmpIdx: number, + cmpBlkIdx: number, + gpioNeeded: number, + gpioAssigned: string[] + } + function copyDoubleArray(a: string[][]) { + return a.map(b => b.map(p => p)); + } + function readPin(arg: string): string { + U.assert(!!arg, "Invalid pin: " + arg); + let pin = arg.split("DigitalPin.")[1]; + return pin; + } + function mkReverseMap(map: {[key: string]: string}) { + let origKeys: string[] = []; + let origVals: string[] = []; + for (let key in map) { + origKeys.push(key); + origVals.push(map[key]); + } + let newMap: {[key: string]: string} = {}; + for (let i = 0; i < origKeys.length; i++) { + let newKey = origVals[i]; + let newVal = origKeys[i]; + newMap[newKey] = newVal; + } + return newMap; + } + class Allocator { + private opts: AllocatorOpts; + private availablePowerPins = { + top: { + threeVolt: mkRange(26, 51).map(n => ["+", `${n}`]), + ground: mkRange(26, 51).map(n => ["-", `${n}`]), + }, + bottom: { + threeVolt: mkRange(1, 26).map(n => ["+", `${n}`]), + ground: mkRange(1, 26).map(n => ["-", `${n}`]), + }, + }; + + constructor(opts: AllocatorOpts) { + this.opts = opts; + } + + private allocateLocation(location: WireLocationDefinition, opts: AllocLocOpts): Loc { + if (location === "ground" || location === "threeVolt") { + U.assert(!!opts.nearestBBPin); + let nearLoc = opts.nearestBBPin; + let nearestCoord = this.opts.getBBCoord(nearLoc); + let firstTopAndBot = [ + this.availablePowerPins.top.ground[0] || this.availablePowerPins.top.threeVolt[0], + this.availablePowerPins.bottom.ground[0] || this.availablePowerPins.bottom.threeVolt[0] + ].map(loc => { + return this.opts.getBBCoord(loc); + }); + if (!firstTopAndBot[0] || !firstTopAndBot[1]) { + console.debug(`No more available "${location}" locations!`); + //TODO + } + let nearTop = visuals.findClosestCoordIdx(nearestCoord, firstTopAndBot) == 0; + let pins: BBRowCol[]; + if (nearTop) { + if (location === "ground") { + pins = this.availablePowerPins.top.ground; + } else if (location === "threeVolt") { + pins = this.availablePowerPins.top.threeVolt; + } + } else { + if (location === "ground") { + pins = this.availablePowerPins.bottom.ground; + } else if (location === "threeVolt") { + pins = this.availablePowerPins.bottom.threeVolt; + } + } + let pinCoords = pins.map(rowCol => { + return this.opts.getBBCoord(rowCol); + }); + let pinIdx = visuals.findClosestCoordIdx(nearestCoord, pinCoords); + let pin = pins[pinIdx]; + if (nearTop) { + this.availablePowerPins.top.ground.splice(pinIdx, 1); + this.availablePowerPins.top.threeVolt.splice(pinIdx, 1); + } else { + this.availablePowerPins.bottom.ground.splice(pinIdx, 1); + this.availablePowerPins.bottom.threeVolt.splice(pinIdx, 1); + } + return {type: "breadboard", rowCol: pin}; + } else if (location[0] === "breadboard") { + U.assert(!!opts.startColumn); + let row = location[1]; + let col = (location[2] + opts.startColumn).toString(); + return {type: "breadboard", rowCol: [row, col]} + } else if (location[0] === "GPIO") { + U.assert(!!opts.cmpGPIOPins); + let idx = location[1]; + let pin = opts.cmpGPIOPins[idx]; + return {type: "dalboard", pin: pin}; + } else { + //TODO + U.assert(false); + return null; + } + } + private allocatePowerWires(): WireInst[] { + let boardGround = this.opts.boardDef.groundPins[0] || null; + if (!boardGround) { + console.log("No available ground pin on board!"); + //TODO + } + let threeVoltPin = this.opts.boardDef.threeVoltPins[0] || null; + if (!threeVoltPin) { + console.log("No available 3.3V pin on board!"); + //TODO + } + let topLeft: BBRowCol = ["-", "26"]; + let botLeft: BBRowCol = ["-", "1"]; + let topRight: BBRowCol = ["-", "50"]; + let botRight: BBRowCol = ["-", "25"]; + let top: BBRowCol, bot: BBRowCol; + if (this.opts.boardDef.attachPowerOnRight) { + top = topRight; + bot = botRight; + } else { + top = topLeft; + bot = botLeft; + } + const GROUND_COLOR = "blue"; + const POWER_COLOR = "red"; + const wires: WireInst[] = [ + {start: this.allocateLocation("ground", {nearestBBPin: top}), + end: this.allocateLocation("ground", {nearestBBPin: bot}), + color: GROUND_COLOR, assemblyStep: 0}, + {start: this.allocateLocation("ground", {nearestBBPin: top}), + end: {type: "dalboard", pin: boardGround}, + color: GROUND_COLOR, assemblyStep: 0}, + {start: this.allocateLocation("threeVolt", {nearestBBPin: top}), + end: this.allocateLocation("threeVolt", {nearestBBPin: bot}), + color: POWER_COLOR, assemblyStep: 1}, + {start: this.allocateLocation("threeVolt", {nearestBBPin: top}), + end: {type: "dalboard", pin: threeVoltPin}, + color: POWER_COLOR, assemblyStep: 1}, + ]; + return wires; + } + private allocateWire(wireDef: WireDefinition, opts: AllocWireOpts): WireInst { + let ends = [wireDef.start, wireDef.end]; + let endIsPower = ends.map(e => e === "ground" || e === "threeVolt"); + let endInsts = ends.map((e, idx) => !endIsPower[idx] ? this.allocateLocation(e, opts) : null) + endInsts = endInsts.map((e, idx) => { + if (e) + return e; + let locInst = endInsts[1 - idx]; + let l = this.allocateLocation(ends[idx], { + nearestBBPin: locInst.rowCol, + startColumn: opts.startColumn, + cmpGPIOPins: opts.cmpGPIOPins + }); + return l; + }); + return {start: endInsts[0], end: endInsts[1], color: wireDef.color, assemblyStep: wireDef.assemblyStep}; + } + private allocatePartialCmps(): PartialCmpAlloc[] { + let cmpNmAndDefs = this.opts.cmpList.map(cmpName => <[string, PartDefinition]>[cmpName, this.opts.cmpDefs[cmpName]]).filter(d => !!d[1]); + let cmpNmsList = cmpNmAndDefs.map(p => p[0]); + let cmpDefsList = cmpNmAndDefs.map(p => p[1]); + let partialCmps: PartialCmpAlloc[] = []; + cmpDefsList.forEach((def, idx) => { + let nm = cmpNmsList[idx]; + if (def.pinAllocation.type === "predefined") { + let mbPins = (def.pinAllocation).pins; + let pinsAssigned = mbPins.map(p => this.opts.boardDef.gpioPinMap[p]); + partialCmps.push({ + name: nm, + def: def, + pinsAssigned: pinsAssigned, + pinsNeeded: 0, + breadboardColumnsNeeded: def.breadboardColumnsNeeded, + }); + } else if (def.pinAllocation.type === "factoryfunction") { + let fnPinAlloc = (def.pinAllocation); + let fnNm = fnPinAlloc.functionName; + let fnsAndArgs = this.opts.fnArgs[fnNm]; + let success = false; + if (fnsAndArgs && fnsAndArgs.length) { + let pinArgPoses = fnPinAlloc.pinArgPositions; + let otherArgPoses = fnPinAlloc.otherArgPositions || []; + fnsAndArgs.forEach(fnArgsStr => { + let fnArgsSplit = fnArgsStr.split(","); + let pinArgs: string[] = []; + pinArgPoses.forEach(i => { + pinArgs.push(fnArgsSplit[i]); + }); + let mbPins = pinArgs.map(arg => readPin(arg)); + let otherArgs: string[] = []; + otherArgPoses.forEach(i => { + otherArgs.push(fnArgsSplit[i]); + }); + let pinsAssigned = mbPins.map(p => this.opts.boardDef.gpioPinMap[p]); + partialCmps.push({ + name: nm, + def: def, + pinsAssigned: pinsAssigned, + pinsNeeded: 0, + breadboardColumnsNeeded: def.breadboardColumnsNeeded, + otherArgs: otherArgs.length ? otherArgs : null, + }); + }); + } else { + // failed to find pin allocation from callsites + console.debug("Failed to read pin(s) from callsite for: " + fnNm); + let pinsNeeded = fnPinAlloc.pinArgPositions.length; + partialCmps.push({ + name: nm, + def: def, + pinsAssigned: [], + pinsNeeded: pinsNeeded, + breadboardColumnsNeeded: def.breadboardColumnsNeeded, + }); + } + } else if (def.pinAllocation.type === "auto") { + let pinsNeeded = (def.pinAllocation).gpioPinsNeeded; + partialCmps.push({ + name: nm, + def: def, + pinsAssigned: [], + pinsNeeded: pinsNeeded, + breadboardColumnsNeeded: def.breadboardColumnsNeeded, + }); + } + }); + return partialCmps; + } + private allocateGPIOPins(partialCmps: PartialCmpAlloc[]): string[][] { + let availableGPIOBlocks = copyDoubleArray(this.opts.boardDef.gpioPinBlocks); + let sortAvailableGPIOBlocks = () => availableGPIOBlocks.sort((a, b) => a.length - b.length); //smallest blocks first + // determine blocks needed + let blockAssignments: AllocBlock[] = []; + let preassignedPins: string[] = []; + partialCmps.forEach((cmp, idx) => { + if (cmp.pinsAssigned && cmp.pinsAssigned.length) { + //already assigned + blockAssignments.push({cmpIdx: idx, cmpBlkIdx: 0, gpioNeeded: 0, gpioAssigned: cmp.pinsAssigned}); + preassignedPins = preassignedPins.concat(cmp.pinsAssigned); + } else if (cmp.pinsNeeded) { + if (typeof cmp.pinsNeeded === "number") { + //individual pins + for (let i = 0; i < cmp.pinsNeeded; i++) { + blockAssignments.push( + {cmpIdx: idx, cmpBlkIdx: 0, gpioNeeded: 1, gpioAssigned: []}); + } + } else { + //blocks of pins + let blocks = cmp.pinsNeeded; + blocks.forEach((numNeeded, blkIdx) => { + blockAssignments.push( + {cmpIdx: idx, cmpBlkIdx: blkIdx, gpioNeeded: numNeeded, gpioAssigned: []}); + }); + } + } + }); + // remove assigned blocks + availableGPIOBlocks.forEach(blks => { + for (let i = blks.length - 1; 0 <= i; i--) { + let pin = blks[i]; + if (0 <= preassignedPins.indexOf(pin)) { + blks.splice(i, 1); + } + } + }); + // sort by size of blocks + let sortBlockAssignments = () => blockAssignments.sort((a, b) => b.gpioNeeded - a.gpioNeeded); //largest blocks first + // allocate each block + if (0 < blockAssignments.length && 0 < availableGPIOBlocks.length) { + do { + sortBlockAssignments(); + sortAvailableGPIOBlocks(); + let assignment = blockAssignments[0]; + let smallestAvailableBlockThatFits: string[]; + for (let j = 0; j < availableGPIOBlocks.length; j++) { + smallestAvailableBlockThatFits = availableGPIOBlocks[j]; + if (assignment.gpioNeeded <= availableGPIOBlocks[j].length) { + break; + } + } + if (!smallestAvailableBlockThatFits || smallestAvailableBlockThatFits.length <= 0) { + break; // out of pins + } + while (0 < assignment.gpioNeeded && 0 < smallestAvailableBlockThatFits.length) { + assignment.gpioNeeded--; + let pin = smallestAvailableBlockThatFits[0]; + smallestAvailableBlockThatFits.splice(0, 1); + assignment.gpioAssigned.push(pin); + } + sortBlockAssignments(); + } while (0 < blockAssignments[0].gpioNeeded); + } + if (0 < blockAssignments.length && 0 < blockAssignments[0].gpioNeeded) { + console.debug("Not enough GPIO pins!"); + return null; + } + let cmpGPIOPinBlocks: string[][][] = partialCmps.map((def, cmpIdx) => { + if (!def) + return null; + let assignments = blockAssignments.filter(a => a.cmpIdx === cmpIdx); + let gpioPins: string[][] = []; + for (let i = 0; i < assignments.length; i++) { + let a = assignments[i]; + let blk = gpioPins[a.cmpBlkIdx] || (gpioPins[a.cmpBlkIdx] = []); + a.gpioAssigned.forEach(p => blk.push(p)); + } + return gpioPins; + }); + let cmpGPIOPins = cmpGPIOPinBlocks.map(blks => blks.reduce((p, n) => p.concat(n), [])); + return cmpGPIOPins; + } + private allocateColumns(partialCmps: PartialCmpAlloc[]): number[] { + let componentsCount = partialCmps.length; + let totalAvailableSpace = 30; //TODO allow multiple breadboards + let totalSpaceNeeded = partialCmps.map(d => d.breadboardColumnsNeeded).reduce((p, n) => p + n, 0); + let extraSpace = totalAvailableSpace - totalSpaceNeeded; + if (extraSpace <= 0) { + console.log("Not enough breadboard space!"); + //TODO + } + let padding = Math.floor(extraSpace / (componentsCount - 1 + 2)); + let componentSpacing = padding; //Math.floor(extraSpace/(componentsCount-1)); + let totalCmpPadding = extraSpace - componentSpacing * (componentsCount - 1); + let leftPadding = Math.floor(totalCmpPadding / 2); + let rightPadding = Math.ceil(totalCmpPadding / 2); + let nextAvailableCol = 1 + leftPadding; + let cmpStartCol = partialCmps.map(cmp => { + let col = nextAvailableCol; + nextAvailableCol += cmp.breadboardColumnsNeeded + componentSpacing; + return col; + }); + return cmpStartCol; + } + private allocateComponent(partialCmp: PartialCmpAlloc, startColumn: number, microbitPins: string[]): CmpInst { + return { + name: partialCmp.name, + breadboardStartColumn: startColumn, + breadboardStartRow: partialCmp.def.breadboardStartRow, + assemblyStep: partialCmp.def.assemblyStep, + visual: partialCmp.def.visual, + microbitPins: microbitPins, + otherArgs: partialCmp.otherArgs, + }; + } + public allocateAll(): AllocatorResult { + let cmpList = this.opts.cmpList; + let basicWires: WireInst[] = []; + let cmpsAndWires: CmpAndWireInst[] = []; + if (cmpList.length > 0) { + basicWires = this.allocatePowerWires(); + let partialCmps = this.allocatePartialCmps(); + let cmpGPIOPins = this.allocateGPIOPins(partialCmps); + let reverseMap = mkReverseMap(this.opts.boardDef.gpioPinMap); + let cmpMicrobitPins = cmpGPIOPins.map(pins => pins.map(p => reverseMap[p])); + let cmpStartCol = this.allocateColumns(partialCmps); + let cmps = partialCmps.map((c, idx) => this.allocateComponent(c, cmpStartCol[idx], cmpMicrobitPins[idx])); + let wires = partialCmps.map((c, idx) => c.def.wires.map(d => this.allocateWire(d, { + cmpGPIOPins: cmpGPIOPins[idx], + startColumn: cmpStartCol[idx], + }))); + cmpsAndWires = cmps.map((c, idx) => { + return {component: c, wires: wires[idx]} + }); + } + return { + powerWires: basicWires, + components: cmpsAndWires + }; + } + } + + export function allocateDefinitions(opts: AllocatorOpts): AllocatorResult { + return new Allocator(opts).allocateAll(); + } +} \ No newline at end of file diff --git a/sim/dalboard.ts b/sim/dalboard.ts new file mode 100644 index 00000000..78fdf4dd --- /dev/null +++ b/sim/dalboard.ts @@ -0,0 +1,96 @@ +namespace pxsim { + export class DalBoard extends BaseBoard { + id: string; + + // the bus + bus: EventBus; + + // state & update logic for component services + ledMatrixState: LedMatrixState; + edgeConnectorState: EdgeConnectorState; + serialState: SerialState; + accelerometerState: AccelerometerState; + compassState: CompassState; + thermometerState: ThermometerState; + lightSensorState: LightSensorState; + buttonPairState: ButtonPairState; + radioState: RadioState; + neopixelState: NeoPixelState; + + // updates + updateSubscribers: (() => void)[]; + + constructor() { + super() + this.id = "b" + Math_.random(2147483647); + this.bus = new EventBus(runtime); + + // components + this.ledMatrixState = new LedMatrixState(runtime); + this.buttonPairState = new ButtonPairState(); + this.edgeConnectorState = new EdgeConnectorState(); + this.radioState = new RadioState(runtime); + this.accelerometerState = new AccelerometerState(runtime); + this.serialState = new SerialState(); + this.thermometerState = new ThermometerState(); + this.lightSensorState = new LightSensorState(); + this.compassState = new CompassState(); + this.neopixelState = new NeoPixelState(); + + // updates + this.updateSubscribers = [] + this.updateView = () => { + this.updateSubscribers.forEach(sub => sub()); + } + } + + receiveMessage(msg: SimulatorMessage) { + if (!runtime || runtime.dead) return; + + switch (msg.type || "") { + case "eventbus": + let ev = msg; + this.bus.queue(ev.id, ev.eventid, ev.value); + break; + case "serial": + let data = (msg).data || ""; + this.serialState.recieveData(data); + break; + case "radiopacket": + let packet = msg; + this.radioState.recievePacket(packet); + break; + } + } + + kill() { + super.kill(); + AudioContextManager.stop(); + } + + initAsync(msg: SimulatorRunMessage): Promise { + let options = (msg.options || {}) as RuntimeOptions; + + let boardDef = CURRENT_BOARD; //TODO: read from pxt.json/pxttarget.json + + let cmpsList = msg.parts; + let cmpDefs = PART_DEFINITIONS; //TODO: read from pxt.json/pxttarget.json + let fnArgs = msg.fnArgs; + + let viewHost = new visuals.BoardHost({ + state: this, + boardDef: boardDef, + cmpsList: cmpsList, + cmpDefs: cmpDefs, + fnArgs: fnArgs, + maxWidth: "100%", + maxHeight: "100%", + }); + + document.body.innerHTML = ""; // clear children + document.body.appendChild(viewHost.getView()); + + return Promise.resolve(); + } + } +} \ No newline at end of file diff --git a/sim/definitions.ts b/sim/definitions.ts new file mode 100644 index 00000000..d4bf1933 --- /dev/null +++ b/sim/definitions.ts @@ -0,0 +1,214 @@ +/// +/// +/// +/// + +namespace pxsim { + export interface PinBlockDefinition { + x: number, + y: number, + labelPosition: "above" | "below"; + labels: string[] + } + export interface BoardImageDefinition { + image: string, + outlineImage?: string, + width: number, + height: number, + pinDist: number, + pinBlocks: PinBlockDefinition[], + }; + export interface BoardDefinition { + visual: BoardImageDefinition | string, + gpioPinBlocks?: string[][], + gpioPinMap: {[pin: string]: string}, + groundPins: string[], + threeVoltPins: string[], + attachPowerOnRight?: boolean, + onboardComponents?: string[] + useCrocClips?: boolean, + marginWhenBreadboarding?: [number, number, number, number], + } + export interface FactoryFunctionPinAlloc { + type: "factoryfunction", + functionName: string, + pinArgPositions: number[], + otherArgPositions?: number[], + } + export interface PredefinedPinAlloc { + type: "predefined", + pins: string[], + } + export interface AutoPinAlloc { + type: "auto", + gpioPinsNeeded: number | number[], + } + export interface PartVisualDefinition { + image: string, + width: number, + height: number, + pinDist: number, + extraColumnOffset?: number, + firstPin: [number, number], + } + export interface PartDefinition { + visual: string | PartVisualDefinition, + breadboardColumnsNeeded: number, + breadboardStartRow: string, + wires: WireDefinition[], + assemblyStep: number, + pinAllocation: FactoryFunctionPinAlloc | PredefinedPinAlloc | AutoPinAlloc, + } + export interface WireDefinition { + start: WireLocationDefinition, + end: WireLocationDefinition, + color: string, + assemblyStep: number + }; + export type WireLocationDefinition = + ["breadboard", string, number] | ["GPIO", number] | "ground" | "threeVolt"; + + export const MICROBIT_DEF: BoardDefinition = { + visual: "microbit", + gpioPinBlocks: [ + ["P0"], ["P1"], ["P2"], + ["P3"], + ["P4", "P5", "P6", "P7"], + ["P8", "P9", "P10", "P11", "P12"], + ["P13", "P14", "P15", "P16"], + ["P19", "P20"], + ], + gpioPinMap: { + "P0": "P0", + "P1": "P1", + "P2": "P2", + "P3": "P3", + "P4": "P4", + "P5": "P5", + "P6": "P6", + "P7": "P7", + "P8": "P8", + "P9": "P9", + "P10": "P10", + "P11": "P11", + "P12": "P12", + "P13": "P13", + "P14": "P14", + "P15": "P15", + "P16": "P16", + "P19": "P19", + "P20": "P20", + }, + groundPins: ["GND"], + threeVoltPins: ["+3v3"], + attachPowerOnRight: true, + onboardComponents: ["buttonpair", "ledmatrix", "speaker"], + useCrocClips: true, + marginWhenBreadboarding: [0, 0, 80, 0], + } + + export const PART_DEFINITIONS: Map = { + "ledmatrix": { + visual: "ledmatrix", + breadboardColumnsNeeded: 8, + breadboardStartRow: "h", + pinAllocation: { + type: "auto", + gpioPinsNeeded: [5, 5], + }, + assemblyStep: 0, + wires: [ + {start: ["breadboard", `j`, 0], end: ["GPIO", 5], color: "purple", assemblyStep: 1}, + {start: ["breadboard", `j`, 1], end: ["GPIO", 6], color: "purple", assemblyStep: 1}, + {start: ["breadboard", `j`, 2], end: ["GPIO", 7], color: "purple", assemblyStep: 1}, + {start: ["breadboard", `j`, 3], end: ["GPIO", 8], color: "purple", assemblyStep: 1}, + {start: ["breadboard", `a`, 7], end: ["GPIO", 9], color: "purple", assemblyStep: 1}, + {start: ["breadboard", `a`, 0], end: ["GPIO", 0], color: "green", assemblyStep: 2}, + {start: ["breadboard", `a`, 1], end: ["GPIO", 1], color: "green", assemblyStep: 2}, + {start: ["breadboard", `a`, 2], end: ["GPIO", 2], color: "green", assemblyStep: 2}, + {start: ["breadboard", `a`, 3], end: ["GPIO", 3], color: "green", assemblyStep: 2}, + {start: ["breadboard", `j`, 4], end: ["GPIO", 4], color: "green", assemblyStep: 2}, + ] + }, + "buttonpair": { + visual: "buttonpair", + breadboardColumnsNeeded: 6, + breadboardStartRow: "f", + pinAllocation: { + type: "predefined", + pins: ["P13", "P12"], + }, + assemblyStep: 0, + wires: [ + {start: ["breadboard", "j", 0], end: ["GPIO", 0], color: "yellow", assemblyStep: 1}, + {start: ["breadboard", "a", 2], end: "ground", color: "blue", assemblyStep: 1}, + {start: ["breadboard", "j", 3], end: ["GPIO", 1], color: "orange", assemblyStep: 2}, + {start: ["breadboard", "a", 5], end: "ground", color: "blue", assemblyStep: 2}, + ], + }, + "neopixel": { + visual: "neopixel", + breadboardColumnsNeeded: 5, + breadboardStartRow: "h", + pinAllocation: { + type: "factoryfunction", + functionName: "neopixel.create", + pinArgPositions: [0], + otherArgPositions: [1], + }, + assemblyStep: 0, + wires: [ + {start: ["breadboard", "j", 1], end: "ground", color: "blue", assemblyStep: 1}, + {start: ["breadboard", "j", 2], end: "threeVolt", color: "red", assemblyStep: 2}, + {start: ["breadboard", "j", 3], end: ["GPIO", 0], color: "green", assemblyStep: 2}, + ], + }, + "speaker": { + visual: { + image: "/static/hardware/speaker.svg", + width: 500, + height: 500, + firstPin: [180, 135], + pinDist: 70, + extraColumnOffset: 1, + }, + breadboardColumnsNeeded: 5, + breadboardStartRow: "f", + pinAllocation: { + type: "auto", + gpioPinsNeeded: 1, + }, + assemblyStep: 0, + wires: [ + {start: ["breadboard", "j", 1], end: ["GPIO", 0], color: "#ff80fa", assemblyStep: 1}, + {start: ["breadboard", "j", 3], end: "ground", color: "blue", assemblyStep: 1}, + ], + }, + } + + export const builtinComponentSimVisual: Map<() => visuals.IBoardComponent> = { + "buttonpair": () => new visuals.ButtonPairView(), + "ledmatrix": () => new visuals.LedMatrixView(), + "neopixel": () => new visuals.NeoPixelView(), + }; + export const builtinComponentSimState: Map<(d: DalBoard) => any> = { + "buttonpair": (d: DalBoard) => d.buttonPairState, + "ledmatrix": (d: DalBoard) => d.ledMatrixState, + "edgeconnector": (d: DalBoard) => d.edgeConnectorState, + "serial": (d: DalBoard) => d.serialState, + "radio": (d: DalBoard) => d.radioState, + "thermometer": (d: DalBoard) => d.thermometerState, + "accelerometer": (d: DalBoard) => d.accelerometerState, + "compass": (d: DalBoard) => d.compassState, + "lightsensor": (d: DalBoard) => d.lightSensorState, + "neopixel": (d: DalBoard) => d.neopixelState, + }; + export const builtinComponentPartVisual: Map<(xy: visuals.Coord) => visuals.SVGElAndSize> = { + "buttonpair": (xy: visuals.Coord) => visuals.mkBtnSvg(xy), + "ledmatrix": (xy: visuals.Coord) => visuals.mkLedMatrixSvg(xy, 8, 8), + "neopixel": (xy: visuals.Coord) => visuals.mkNeoPixelPart(xy), + }; + + //TODO: add multiple board support + export const CURRENT_BOARD = MICROBIT_DEF; +} \ No newline at end of file diff --git a/sim/instructions/instructions.ts b/sim/instructions/instructions.ts new file mode 100644 index 00000000..25cb44ec --- /dev/null +++ b/sim/instructions/instructions.ts @@ -0,0 +1,672 @@ +/// +/// +/// +/// +/// +/// + +//HACK: allows instructions.html to access pxtblocks without requiring simulator.html to import blocks as well +if (!(window).pxt) (window).pxt = {}; +import pxtrunner = pxt.runner; +import pxtdocs = pxt.docs; + +namespace pxsim.instructions { + const LOC_LBL_SIZE = 10; + const QUANT_LBL_SIZE = 30; + const QUANT_LBL = (q: number) => `${q}x`; + const WIRE_QUANT_LBL_SIZE = 20; + const LBL_VERT_PAD = 3; + const LBL_RIGHT_PAD = 5; + const LBL_LEFT_PAD = 5; + const REQ_WIRE_HEIGHT = 45; + const REQ_CMP_HEIGHT = 55; + const REQ_CMP_SCALE = 0.5; + type Orientation = "landscape" | "portrait"; + const ORIENTATION: Orientation = "portrait"; + const PPI = 96.0; + const [FULL_PAGE_WIDTH, FULL_PAGE_HEIGHT] + = (ORIENTATION == "portrait" ? [PPI * 8.5, PPI * 11.0] : [PPI * 11.0, PPI * 8.5]); + const PAGE_MARGIN = PPI * 0.45; + const PAGE_WIDTH = FULL_PAGE_WIDTH - PAGE_MARGIN * 2; + const PAGE_HEIGHT = FULL_PAGE_HEIGHT - PAGE_MARGIN * 2; + const BORDER_COLOR = "gray"; + const BORDER_RADIUS = 5; + const BORDER_WIDTH = 2; + const [PANEL_ROWS, PANEL_COLS] = [2, 2]; + const PANEL_MARGIN = 20; + const PANEL_PADDING = 8; + const PANEL_WIDTH = PAGE_WIDTH / PANEL_COLS - (PANEL_MARGIN + PANEL_PADDING + BORDER_WIDTH) * PANEL_COLS; + const PANEL_HEIGHT = PAGE_HEIGHT / PANEL_ROWS - (PANEL_MARGIN + PANEL_PADDING + BORDER_WIDTH) * PANEL_ROWS; + const BOARD_WIDTH = 240; + const BOARD_LEFT = (PANEL_WIDTH - BOARD_WIDTH) / 2.0 + PANEL_PADDING; + const BOARD_BOT = PANEL_PADDING; + const NUM_BOX_SIZE = 60; + const NUM_FONT = 40; + const NUM_MARGIN = 5; + const FRONT_PAGE_BOARD_WIDTH = 200; + const PARTS_BOARD_SCALE = 0.17; + const PARTS_BB_SCALE = 0.25; + const PARTS_CMP_SCALE = 0.3; + const PARTS_WIRE_SCALE = 0.23; + const STYLE = ` + .instr-panel { + margin: ${PANEL_MARGIN}px; + padding: ${PANEL_PADDING}px; + border-width: ${BORDER_WIDTH}px; + border-color: ${BORDER_COLOR}; + border-style: solid; + border-radius: ${BORDER_RADIUS}px; + display: inline-block; + width: ${PANEL_WIDTH}px; + height: ${PANEL_HEIGHT}px; + position: relative; + overflow: hidden; + } + .board-svg { + margin: 0 auto; + display: block; + position: absolute; + bottom: ${BOARD_BOT}px; + left: ${BOARD_LEFT}px; + } + .panel-num-outer { + position: absolute; + left: ${-BORDER_WIDTH}px; + top: ${-BORDER_WIDTH}px; + width: ${NUM_BOX_SIZE}px; + height: ${NUM_BOX_SIZE}px; + border-width: ${BORDER_WIDTH}px; + border-style: solid; + border-color: ${BORDER_COLOR}; + border-radius: ${BORDER_RADIUS}px 0 ${BORDER_RADIUS}px 0; + } + .panel-num { + margin: ${NUM_MARGIN}px 0; + text-align: center; + font-size: ${NUM_FONT}px; + } + .cmp-div { + display: inline-block; + } + .reqs-div { + margin-left: ${PANEL_PADDING + NUM_BOX_SIZE}px; + } + .partslist-wire, + .partslist-cmp { + margin: 5px; + } + .partslist-wire { + display: inline-block; + } + `; + + function addClass(el: HTMLElement, cls: string) { + //TODO move to library + if (el.classList) el.classList.add(cls); + //BUG: won't work if element has class that is prefix of new class + //TODO: make github issue (same issue exists svg.addClass) + else if (!el.className.indexOf(cls)) el.className += " " + cls; + } + function mkTxt(p: [number, number], txt: string, size: number) { + let el = svg.elt("text") + let [x, y] = p; + svg.hydrate(el, { x: x, y: y, style: `font-size:${size}px;` }); + el.textContent = txt; + return el; + } + type mkCmpDivOpts = { + top?: string, + topSize?: number, + right?: string, + rightSize?: number, + left?: string, + leftSize?: number, + bot?: string, + botSize?: number, + wireClr?: string, + cmpWidth?: number, + cmpHeight?: number, + cmpScale?: number + }; + function mkBoardImgSvg(def: string | BoardImageDefinition): visuals.SVGElAndSize { + let boardView: visuals.BoardView; + if (def === "microbit") { + boardView = new visuals.MicrobitBoardSvg({ + theme: visuals.randomTheme() + }) + } else { + boardView = new visuals.GenericBoardSvg({ + visualDef: def + }) + } + return boardView.getView(); + } + function mkBBSvg(): visuals.SVGElAndSize { + let bb = new visuals.Breadboard({}); + return bb.getSVGAndSize(); + } + function wrapSvg(el: visuals.SVGElAndSize, opts: mkCmpDivOpts): HTMLElement { + //TODO: Refactor this function; it is too complicated. There is a lot of error-prone math being done + // to scale and place all elements which could be simplified with more forethought. + let svgEl = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + let dims = {l: 0, t: 0, w: 0, h: 0}; + + let cmpSvgEl = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svgEl.appendChild(cmpSvgEl); + + cmpSvgEl.appendChild(el.el); + let cmpSvgAtts = { + "viewBox": `${el.x} ${el.y} ${el.w} ${el.h}`, + "preserveAspectRatio": "xMidYMid", + }; + dims.w = el.w; + dims.h = el.h; + let scale = (scaler: number) => { + dims.h *= scaler; + dims.w *= scaler; + (cmpSvgAtts).width = dims.w; + (cmpSvgAtts).height = dims.h; + } + if (opts.cmpScale) { + scale(opts.cmpScale) + } + if (opts.cmpWidth && opts.cmpWidth < dims.w) { + scale(opts.cmpWidth / dims.w); + } else if (opts.cmpHeight && opts.cmpHeight < dims.h) { + scale(opts.cmpHeight / dims.h) + } + svg.hydrate(cmpSvgEl, cmpSvgAtts); + let elDims = {l: dims.l, t: dims.t, w: dims.w, h: dims.h}; + + let updateL = (newL: number) => { + if (newL < dims.l) { + let extraW = dims.l - newL; + dims.l = newL; + dims.w += extraW; + } + } + let updateR = (newR: number) => { + let oldR = dims.l + dims.w; + if (oldR < newR) { + let extraW = newR - oldR; + dims.w += extraW; + } + } + let updateT = (newT: number) => { + if (newT < dims.t) { + let extraH = dims.t - newT; + dims.t = newT; + dims.h += extraH; + } + } + let updateB = (newB: number) => { + let oldB = dims.t + dims.h; + if (oldB < newB) { + let extraH = newB - oldB; + dims.h += extraH; + } + } + + //labels + let [xOff, yOff] = [-0.3, 0.3]; //HACK: these constants tweak the way "mkTxt" knows how to center the text + const txtAspectRatio = [1.4, 1.0]; + if (opts && opts.top) { + let size = opts.topSize; + let txtW = size / txtAspectRatio[0]; + let txtH = size / txtAspectRatio[1]; + let [cx, y] = [elDims.l + elDims.w / 2, elDims.t - LBL_VERT_PAD - txtH / 2]; + let lbl = visuals.mkTxt(cx, y, size, 0, opts.top, xOff, yOff); + svg.addClass(lbl, "cmp-lbl"); + svgEl.appendChild(lbl); + + let len = txtW * opts.top.length; + updateT(y - txtH / 2); + updateL(cx - len / 2); + updateR(cx + len / 2); + } + if (opts && opts.bot) { + let size = opts.botSize; + let txtW = size / txtAspectRatio[0]; + let txtH = size / txtAspectRatio[1]; + let [cx, y] = [elDims.l + elDims.w / 2, elDims.t + elDims.h + LBL_VERT_PAD + txtH / 2]; + let lbl = visuals.mkTxt(cx, y, size, 0, opts.bot, xOff, yOff); + svg.addClass(lbl, "cmp-lbl"); + svgEl.appendChild(lbl); + + let len = txtW * opts.bot.length; + updateB(y + txtH / 2); + updateL(cx - len / 2); + updateR(cx + len / 2); + } + if (opts && opts.right) { + let size = opts.rightSize; + let txtW = size / txtAspectRatio[0]; + let txtH = size / txtAspectRatio[1]; + let len = txtW * opts.right.length; + let [cx, cy] = [elDims.l + elDims.w + LBL_RIGHT_PAD + len / 2, elDims.t + elDims.h / 2]; + let lbl = visuals.mkTxt(cx, cy, size, 0, opts.right, xOff, yOff); + svg.addClass(lbl, "cmp-lbl"); + svgEl.appendChild(lbl); + + updateT(cy - txtH / 2); + updateR(cx + len / 2); + updateB(cy + txtH / 2); + } + if (opts && opts.left) { + let size = opts.leftSize; + let txtW = size / txtAspectRatio[0]; + let txtH = size / txtAspectRatio[1]; + let len = txtW * opts.left.length; + let [cx, cy] = [elDims.l - LBL_LEFT_PAD - len / 2, elDims.t + elDims.h / 2]; + let lbl = visuals.mkTxt(cx, cy, size, 0, opts.left, xOff, yOff); + svg.addClass(lbl, "cmp-lbl"); + svgEl.appendChild(lbl); + + updateT(cy - txtH / 2); + updateL(cx - len / 2); + updateB(cy + txtH / 2); + } + + let svgAtts = { + "viewBox": `${dims.l} ${dims.t} ${dims.w} ${dims.h}`, + "width": dims.w, + "height": dims.h, + "preserveAspectRatio": "xMidYMid", + }; + svg.hydrate(svgEl, svgAtts); + let div = document.createElement("div"); + div.appendChild(svgEl); + return div; + } + function mkCmpDiv(cmp: "wire" | string | PartVisualDefinition, opts: mkCmpDivOpts): HTMLElement { + let el: visuals.SVGElAndSize; + if (cmp == "wire") { + //TODO: support non-croc wire parts + el = visuals.mkWirePart([0, 0], opts.wireClr || "red", true); + } else if (typeof cmp == "string") { + let builtinVis = cmp; + let cnstr = builtinComponentPartVisual[builtinVis]; + el = cnstr([0, 0]); + } else { + let partVis = cmp; + el = visuals.mkGenericPartSVG(partVis); + } + return wrapSvg(el, opts); + } + type BoardProps = { + boardDef: BoardDefinition, + cmpDefs: Map, + fnArgs: any, + allAlloc: AllocatorResult, + stepToWires: WireInst[][], + stepToCmps: CmpInst[][] + allWires: WireInst[], + allCmps: CmpInst[], + lastStep: number, + colorToWires: Map, + allWireColors: string[], + }; + function mkBoardProps(allocOpts: AllocatorOpts): BoardProps { + let allocRes = allocateDefinitions(allocOpts); + let {powerWires, components} = allocRes; + let stepToWires: WireInst[][] = []; + let stepToCmps: CmpInst[][] = []; + powerWires.forEach(w => { + let step = w.assemblyStep + 1; + (stepToWires[step] || (stepToWires[step] = [])).push(w) + }); + let getMaxStep = (ns: {assemblyStep: number}[]) => ns.reduce((m, n) => Math.max(m, n.assemblyStep), 0); + let stepOffset = getMaxStep(powerWires) + 2; + components.forEach(cAndWs => { + let {component, wires} = cAndWs; + let cStep = component.assemblyStep + stepOffset; + let arr = stepToCmps[cStep] || (stepToCmps[cStep] = []); + arr.push(component); + let wSteps = wires.map(w => w.assemblyStep + stepOffset); + wires.forEach((w, i) => { + let wStep = wSteps[i]; + let arr = stepToWires[wStep] || (stepToWires[wStep] = []); + arr.push(w); + }) + stepOffset = Math.max(cStep, wSteps.reduce((m, n) => Math.max(m, n), 0)) + 1; + }); + let lastStep = stepOffset - 1; + let allCmps = components.map(p => p.component); + let allWires = powerWires.concat(components.map(p => p.wires).reduce((p, n) => p.concat(n), [])); + let colorToWires: Map = {} + let allWireColors: string[] = []; + allWires.forEach(w => { + if (!colorToWires[w.color]) { + colorToWires[w.color] = []; + allWireColors.push(w.color); + } + colorToWires[w.color].push(w); + }); + return { + boardDef: allocOpts.boardDef, + cmpDefs: allocOpts.cmpDefs, + fnArgs: allocOpts.fnArgs, + allAlloc: allocRes, + stepToWires: stepToWires, + stepToCmps: stepToCmps, + allWires: allWires, + allCmps: allCmps, + lastStep: lastStep, + colorToWires: colorToWires, + allWireColors: allWireColors, + }; + } + function mkBlankBoardAndBreadboard(boardDef: BoardDefinition, cmpDefs: Map, fnArgs: any, width: number, buildMode: boolean = false): visuals.BoardHost { + let state = runtime.board as pxsim.DalBoard; + let boardHost = new visuals.BoardHost({ + state: state, + boardDef: boardDef, + forceBreadboard: true, + cmpDefs: cmpDefs, + maxWidth: `${width}px`, + fnArgs: fnArgs, + wireframe: buildMode, + }); + let view = boardHost.getView(); + svg.addClass(view, "board-svg"); + + //set smiley + //HACK + // let img = board.board.displayCmp.image; + // img.set(1, 0, 255); + // img.set(3, 0, 255); + // img.set(0, 2, 255); + // img.set(1, 3, 255); + // img.set(2, 3, 255); + // img.set(3, 3, 255); + // img.set(4, 2, 255); + // board.updateState(); + + return boardHost; + } + function drawSteps(board: visuals.BoardHost, step: number, props: BoardProps) { + let view = board.getView(); + if (step > 0) { + svg.addClass(view, "grayed"); + } + + for (let i = 0; i <= step; i++) { + let wires = props.stepToWires[i]; + if (wires) { + wires.forEach(w => { + let wire = board.addWire(w) + //last step + if (i === step) { + //location highlights + if (w.start.type == "breadboard") { + let lbls = board.highlightBreadboardPin((w.start).rowCol); + } else { + board.highlightBoardPin((w.start).pin); + } + if (w.end.type == "breadboard") { + let [row, col] = (w.end).rowCol; + let lbls = board.highlightBreadboardPin((w.end).rowCol); + } else { + board.highlightBoardPin((w.end).pin); + } + //highlight wire + board.highlightWire(wire); + } + }); + } + let cmps = props.stepToCmps[i]; + if (cmps) { + cmps.forEach(cmpInst => { + let cmp = board.addComponent(cmpInst) + let colOffset = (cmpInst.visual).breadboardStartColIdx || 0; + let rowCol: BBRowCol = [`${cmpInst.breadboardStartRow}`, `${colOffset + cmpInst.breadboardStartColumn}`]; + //last step + if (i === step) { + board.highlightBreadboardPin(rowCol); + if (cmpInst.visual === "buttonpair") { + //TODO: don't specialize this + let rowCol2: BBRowCol = [`${cmpInst.breadboardStartRow}`, `${cmpInst.breadboardStartColumn + 3}`]; + board.highlightBreadboardPin(rowCol2); + } + svg.addClass(cmp.element, "notgrayed"); + } + }); + } + } + } + function mkPanel() { + //panel + let panel = document.createElement("div"); + addClass(panel, "instr-panel"); + + return panel; + } + function mkPartsPanel(props: BoardProps) { + let panel = mkPanel(); + + // board and breadboard + let boardImg = mkBoardImgSvg(props.boardDef.visual); + let board = wrapSvg(boardImg, {left: QUANT_LBL(1), leftSize: QUANT_LBL_SIZE, cmpScale: PARTS_BOARD_SCALE}); + panel.appendChild(board); + let bbRaw = mkBBSvg(); + let bb = wrapSvg(bbRaw, {left: QUANT_LBL(1), leftSize: QUANT_LBL_SIZE, cmpScale: PARTS_BB_SCALE}); + panel.appendChild(bb); + + // components + let cmps = props.allCmps; + cmps.forEach(c => { + let quant = 1; + // TODO: don't special case this + if (c.visual === "buttonpair") { + quant = 2; + } + let cmp = mkCmpDiv(c.visual, { + left: QUANT_LBL(quant), + leftSize: QUANT_LBL_SIZE, + cmpScale: PARTS_CMP_SCALE, + }); + addClass(cmp, "partslist-cmp"); + panel.appendChild(cmp); + }); + + // wires + props.allWireColors.forEach(clr => { + let quant = props.colorToWires[clr].length; + let cmp = mkCmpDiv("wire", { + left: QUANT_LBL(quant), + leftSize: WIRE_QUANT_LBL_SIZE, + wireClr: clr, + cmpScale: PARTS_WIRE_SCALE + }) + addClass(cmp, "partslist-wire"); + panel.appendChild(cmp); + }) + + return panel; + } + function mkStepPanel(step: number, props: BoardProps) { + let panel = mkPanel(); + + //board + let board = mkBlankBoardAndBreadboard(props.boardDef, props.cmpDefs, props.fnArgs, BOARD_WIDTH, true) + drawSteps(board, step, props); + panel.appendChild(board.getView()); + + //number + let numDiv = document.createElement("div"); + addClass(numDiv, "panel-num-outer"); + addClass(numDiv, "noselect"); + panel.appendChild(numDiv) + let num = document.createElement("div"); + addClass(num, "panel-num"); + num.textContent = (step + 1) + ""; + numDiv.appendChild(num) + + // add requirements + let reqsDiv = document.createElement("div"); + addClass(reqsDiv, "reqs-div") + panel.appendChild(reqsDiv); + let wires = (props.stepToWires[step] || []); + let mkLabel = (loc: Loc) => { + if (loc.type === "breadboard") { + let [row, col] = (loc).rowCol; + return `(${row},${col})` + } else + return (loc).pin; + }; + wires.forEach(w => { + let cmp = mkCmpDiv("wire", { + top: mkLabel(w.end), + topSize: LOC_LBL_SIZE, + bot: mkLabel(w.start), + botSize: LOC_LBL_SIZE, + wireClr: w.color, + cmpHeight: REQ_WIRE_HEIGHT + }) + addClass(cmp, "cmp-div"); + reqsDiv.appendChild(cmp); + }); + let cmps = (props.stepToCmps[step] || []); + cmps.forEach(c => { + let l: BBRowCol = [`${c.breadboardStartRow}`, `${c.breadboardStartColumn}`]; + let locs = [l]; + if (c.visual === "buttonpair") { + //TODO: don't special case this + let l2: BBRowCol = [`${c.breadboardStartRow}`, `${c.breadboardStartColumn + 3}`]; + locs.push(l2); + } + locs.forEach((l, i) => { + let [row, col] = l; + let cmp = mkCmpDiv(c.visual, { + top: `(${row},${col})`, + topSize: LOC_LBL_SIZE, + cmpHeight: REQ_CMP_HEIGHT, + cmpScale: REQ_CMP_SCALE + }) + addClass(cmp, "cmp-div"); + reqsDiv.appendChild(cmp); + }); + }); + + return panel; + } + function updateFrontPanel(props: BoardProps): [HTMLElement, BoardProps] { + let panel = document.getElementById("front-panel"); + + let board = mkBlankBoardAndBreadboard(props.boardDef, props.cmpDefs, props.fnArgs, FRONT_PAGE_BOARD_WIDTH, false); + board.addAll(props.allAlloc); + panel.appendChild(board.getView()); + + return [panel, props]; + } + function mkFinalPanel(props: BoardProps) { + const BACK_PAGE_BOARD_WIDTH = PANEL_WIDTH - 20; + + let panel = mkPanel(); + addClass(panel, "back-panel"); + let board = mkBlankBoardAndBreadboard(props.boardDef, props.cmpDefs, props.fnArgs, BACK_PAGE_BOARD_WIDTH, false) + board.addAll(props.allAlloc); + panel.appendChild(board.getView()); + + return panel; + } + export function drawInstructions() { + let getQsVal = parseQueryString(); + + //project name + let name = getQsVal("name") || "Untitled"; + if (name) { + $("#proj-title").text(name); + } + + //project code + let tsCode = getQsVal("code"); + let tsPackage = getQsVal("package") || ""; + let codeSpinnerDiv = document.getElementById("proj-code-spinner"); + let codeContainerDiv = document.getElementById("proj-code-container"); + if (tsCode) { + //we use the docs renderer to decompile the code to blocks and render it + //TODO: render the blocks code directly + let md = +`\`\`\`blocks +${tsCode} +\`\`\` +\`\`\`package +${tsPackage} +\`\`\` +` + + pxtdocs.requireMarked = function() { return (window).marked; } + pxtrunner.renderMarkdownAsync(codeContainerDiv, md) + .done(function() { + let codeSvg = $("#proj-code-container svg"); + if (codeSvg.length > 0) { + //code rendered successfully as blocks + codeSvg.css("width", "inherit"); + codeSvg.css("height", "inherit"); + //takes the svg out of the wrapper markdown + codeContainerDiv.innerHTML = ""; + codeContainerDiv.appendChild(codeSvg[0]); + } else { + //code failed to convert to blocks, display as typescript instead + codeContainerDiv.innerText = tsCode; + } + $(codeContainerDiv).show(); + $(codeSpinnerDiv).hide(); + }); + } + + //parts list + let parts = (getQsVal("parts") || "").split(" "); + parts.sort(); + + //fn args + let fnArgs = JSON.parse((getQsVal("fnArgs") || "{}")); + + //init runtime + const COMP_CODE = ""; + if (!pxsim.initCurrentRuntime) + pxsim.initCurrentRuntime = initRuntimeWithDalBoard; + pxsim.runtime = new Runtime(COMP_CODE); + pxsim.runtime.board = null; + pxsim.initCurrentRuntime(); + + let style = document.createElement("style"); + document.head.appendChild(style); + + style.textContent += STYLE; + + const boardDef = CURRENT_BOARD; + const cmpDefs = PART_DEFINITIONS; + + //props + let dummyBreadboard = new visuals.Breadboard({}); + let onboardCmps = boardDef.onboardComponents || []; + let activeComponents = (parts || []).filter(c => onboardCmps.indexOf(c) < 0); + activeComponents.sort(); + let props = mkBoardProps({ + boardDef: boardDef, + cmpDefs: cmpDefs, + cmpList: activeComponents, + fnArgs: fnArgs, + getBBCoord: dummyBreadboard.getCoord.bind(dummyBreadboard) + }); + + //front page + let frontPanel = updateFrontPanel(props); + + //all required parts + let partsPanel = mkPartsPanel(props); + document.body.appendChild(partsPanel); + + //steps + for (let s = 0; s <= props.lastStep; s++) { + let p = mkStepPanel(s, props); + document.body.appendChild(p); + } + + //final + let finalPanel = mkFinalPanel(props); + document.body.appendChild(finalPanel); + } +} \ No newline at end of file diff --git a/sim/libmbit.ts b/sim/libmbit.ts deleted file mode 100644 index 7d427d21..00000000 --- a/sim/libmbit.ts +++ /dev/null @@ -1,763 +0,0 @@ -/// -/// -/// - -namespace pxsim { - pxsim.initCurrentRuntime = () => { - U.assert(!runtime.board); - runtime.board = new Board(); - } - - export function board() { - return runtime.board as Board; - } - - 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) - }) - } - } - - /** - * 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().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 function getPin(id: number) { - return board().pins.filter(p => p && p.id == id)[0] || null - } - - - 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; - } - } - - -} - -namespace pxsim.basic { - export var pause = thread.pause; - export var forever = thread.forever; - - 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().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.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 onButtonPressed(button: number, handler: RefAction): void { - let b = board(); - if (button == DAL.MICROBIT_ID_BUTTON_AB && !board().usesButtonAB) { - b.usesButtonAB = true; - runtime.queueDisplayUpdate(); - } - pxt.registerWithDal(button, DAL.MICROBIT_BUTTON_EVT_CLICK, handler); - } - - export function buttonIsPressed(button: number): boolean { - let b = board(); - if (button == DAL.MICROBIT_ID_BUTTON_AB && !board().usesButtonAB) { - b.usesButtonAB = true; - runtime.queueDisplayUpdate(); - } - let bts = b.buttons; - if (button == DAL.MICROBIT_ID_BUTTON_A) return bts[0].pressed; - if (button == DAL.MICROBIT_ID_BUTTON_B) return bts[1].pressed; - return bts[2].pressed || (bts[0].pressed && bts[1].pressed); - } - - export function onGesture(gesture: number, handler: RefAction) { - let b = board(); - b.accelerometer.activate(); - - if (gesture == 11 && !b.useShake) { // SAKE - b.useShake = true; - runtime.queueDisplayUpdate(); - } - pxt.registerWithDal(DAL.MICROBIT_ID_GESTURE, gesture, handler); - } - - 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(); - } - - - - export function compassHeading(): number { - let b = board(); - if (!b.usesHeading) { - b.usesHeading = true; - runtime.queueDisplayUpdate(); - } - return b.heading; - } - - export function temperature(): number { - let b = board(); - if (!b.usesTemperature) { - b.usesTemperature = true; - runtime.queueDisplayUpdate(); - } - return b.temperature; - } - - export function acceleration(dimension: number): number { - let b = board(); - 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(); - 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(); - b.accelerometer.setSampleRange(range); - } - - export function lightLevel(): number { - let b = board(); - if (!b.usesLightLevel) { - b.usesLightLevel = true; - runtime.queueDisplayUpdate(); - } - return b.lightLevel; - } - - export function magneticForce(): number { - // TODO - return 0; - } - - export function runningTime(): number { - return runtime.runningTime(); - } - - export function calibrate() { - } -} - -namespace pxsim.led { - 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 setDisplayMode(mode: DisplayMode): void { - board().displayMode = mode; - runtime.queueDisplayUpdate() - } - - export function screenshot(): Image { - let img = createImage(5) - board().image.copyTo(0, 5, img, 0); - return img; - } -} - -namespace pxsim.serial { - export function writeString(s: string) { - board().writeSerial(s); - } - - export function readString(): string { - return board().readSerial(); - } - - export function readLine(): string { - return board().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? - } -} - - -namespace pxsim.radio { - export function broadcastMessage(msg: number): void { - board().radio.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().radio.setGroup(id); - } - - export function setTransmitPower(power: number): void { - board().radio.setTransmitPower(power); - } - - export function setTransmitSerialNumber(transmit: boolean): void { - board().radio.setTransmitSerialNumber(transmit); - } - - export function sendNumber(value: number): void { - board().radio.datagram.send([value]); - } - - export function sendString(msg: string): void { - board().radio.datagram.send(msg); - } - - export function writeValueToSerial(): void { - let b = board(); - let v = b.radio.datagram.recv().data[0]; - b.writeSerial(`{v:${v}}`); - } - - export function sendValue(name: string, value: number) { - board().radio.datagram.send([value]); - } - - export function receiveNumber(): number { - let buffer = board().radio.datagram.recv().data; - if (buffer instanceof Array) return buffer[0]; - - return 0; - } - - export function receiveString(): string { - let buffer = board().radio.datagram.recv().data; - if (typeof buffer === "string") return buffer; - return ""; - } - - export function receivedNumberAt(index: number): number { - let buffer = board().radio.datagram.recv().data; - if (buffer instanceof Array) return buffer[index] || 0; - - return 0; - } - - export function receivedSignalStrength(): number { - return board().radio.datagram.lastReceived.rssi; - } - - export function onDataReceived(handler: RefAction): void { - pxt.registerWithDal(DAL.MICROBIT_ID_RADIO, DAL.MICROBIT_RADIO_EVT_DATAGRAM, handler); - radio.receiveNumber(); - } -} - -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 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 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 - } - - export function analogSetPitchPin(pinId: number) { - let pin = getPin(pinId); - if (!pin) return; - board().pins.filter(p => !!p).forEach(p => p.pitch = false); - pin.pitch = true; - } - - export function analogPitch(frequency: number, ms: number) { - // update analog output - let pin = board().pins.filter(pin => !!pin && pin.pitch)[0] || board().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); - } - } - - -} - -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 - } -} - -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().image, 0) - runtime.queueDisplayUpdate() - } - - export function plotImage(leds: Image, offset: number): void { - if (!leds) panic(PanicCode.MICROBIT_NULL_DEREFERENCE); - - leds.copyTo(offset, 5, board().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().image; - - board().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 - }) - } -} diff --git a/sim/simsvg.ts b/sim/microbit.ts similarity index 70% rename from sim/simsvg.ts rename to sim/microbit.ts index 9dc6fdf9..1d5d2842 100644 --- a/sim/simsvg.ts +++ b/sim/microbit.ts @@ -1,6 +1,183 @@ -namespace pxsim.micro_bit { - const svg = pxsim.svg; +namespace pxsim.visuals { + const MB_STYLE = ` + svg.sim { + margin-bottom:1em; + } + svg.sim.grayscale { + -moz-filter: grayscale(1); + -webkit-filter: grayscale(1); + filter: grayscale(1); + } + .sim-button { + pointer-events: none; + } + .sim-button-outer:hover { + stroke:grey; + stroke-width: 3px; + } + .sim-button-nut { + fill:#704A4A; + pointer-events:none; + } + .sim-button-nut:hover { + stroke:1px solid #704A4A; + } + .sim-pin: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-light-level-button { + stroke:#fff; + stroke-width: 3px; + } + + .sim-antenna { + stroke:#555; + stroke-width: 2px; + } + + .sim-text { + font-family:"Lucida Console", Monaco, monospace; + font-size:25px; + fill:#fff; + pointer-events: none; + } + + .sim-text-pin { + font-family:"Lucida Console", Monaco, monospace; + font-size:20px; + fill:#fff; + pointer-events: none; + } + + .sim-thermometer { + stroke:#aaa; + stroke-width: 3px; + } + + /* animations */ + .sim-theme-glow { + animation-name: sim-theme-glow-animation; + animation-timing-function: ease-in-out; + animation-direction: alternate; + animation-iteration-count: infinite; + animation-duration: 1.25s; + } + @keyframes sim-theme-glow-animation { + from { opacity: 1; } + to { opacity: 0.75; } + } + + .sim-flash { + animation-name: sim-flash-animation; + animation-duration: 0.1s; + } + + @keyframes sim-flash-animation { + from { fill: yellow; } + to { fill: default; } + } + + .sim-flash-stroke { + 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; } + } + + /* wireframe */ + .sim-wireframe * { + fill: none; + stroke: black; + } + .sim-wireframe .sim-display, + .sim-wireframe .sim-led, + .sim-wireframe .sim-led-back, + .sim-wireframe .sim-head, + .sim-wireframe .sim-theme, + .sim-wireframe .sim-button-group, + .sim-wireframe .sim-button-label, + .sim-wireframe .sim-button, + .sim-wireframe .sim-text-pin + { + visibility: hidden; + } + .sim-wireframe .sim-label + { + stroke: none; + fill: #777; + } + .sim-wireframe .sim-board { + stroke-width: 2px; + } + `; + const pins4onXs = [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]; + const pins4onMids = pins4onXs.map(x => x + 5); + const littlePinDist = pins4onMids[1] - pins4onMids[0]; + const bigPinWidth = pins4onMids[4] - pins4onMids[3]; + const pin0mid = pins4onXs[0] - bigPinWidth / 2.0; + const pin3mid = pin0mid - bigPinWidth / 2.0; + const pin1mid = pins4onMids[3] + bigPinWidth / 2.0; + const pin2mid = pins4onMids[8] + bigPinWidth / 2.0; + const pin3Vmid = pins4onMids[13] + bigPinWidth / 2.0; + const pinGNDmid = pins4onMids[pins4onMids.length - 1] + bigPinWidth / 2.0; + const pinGND2mid = pinGNDmid + bigPinWidth / 2.0; + const pinMids = [pin0mid, pin1mid, pin2mid, pin3mid].concat(pins4onXs).concat([pinGNDmid, pin3Vmid, pinGND2mid]); + const pinNames = [ + "P0", "P1", "P2", "P3", "P4", "P5", "P6", "P7", "P8", "P9", "P10", + "P11", "P12", "P13", "P14", "P15", "P16", "P17", "P18", "P19", "P20", + "GND0", "GND", "+3v3", "GND1"]; + const pinTitles = [ + "P0, ANALOG IN", + "P1, ANALOG IN", + "P2, ANALOG IN", + "P3, ANALOG IN, LED Col 1", + "P4, ANALOG IN, LED Col 2", + "P5, BUTTON A", + "P6, LED Col 9", + "P7, LED Col 8", + "P8", + "P9, LED Col 7", + "P10, ANALOG IN, LED Col 3", + "P11, BUTTON B", + "P12, RESERVED ACCESSIBILITY", + "P13, SPI - SCK", + "P14, SPI - MISO", + "P15, SPI - MOSI", + "P16, SPI - Chip Select", + "P17, +3v3", + "P18, +3v3", + "P19, I2C - SCL", + "P20, I2C - SDA", + "GND", "GND", "+3v3", "GND" + ]; + const MB_WIDTH = 500; + const MB_HEIGHT = 408; export interface IBoardTheme { accent?: string; display?: string; @@ -43,9 +220,10 @@ namespace pxsim.micro_bit { } export interface IBoardProps { - runtime: pxsim.Runtime; + runtime?: pxsim.Runtime; theme?: IBoardTheme; disableTilt?: boolean; + wireframe?: boolean; } const pointerEvents = !!(window as any).PointerEvent ? { @@ -60,11 +238,11 @@ namespace pxsim.micro_bit { leave: "mouseleave" }; - export class MicrobitBoardSvg { + export class MicrobitBoardSvg implements BoardView { public element: SVGSVGElement; private style: SVGStyleElement; private defs: SVGDefsElement; - private g: SVGElement; + private g: SVGGElement; private logos: SVGElement[]; private head: SVGGElement; private headInitialized = false; @@ -88,15 +266,54 @@ namespace pxsim.micro_bit { private thermometerText: SVGTextElement; private shakeButton: SVGCircleElement; private shakeText: SVGTextElement; - public board: pxsim.Board; + public board: pxsim.DalBoard; + private pinNmToCoord: Map = {}; constructor(public props: IBoardProps) { - this.board = this.props.runtime.board as pxsim.Board; - this.board.updateView = () => this.updateState(); + this.recordPinCoords(); this.buildDom(); - this.updateTheme(); - this.updateState(); - this.attachEvents(); + if (props && props.wireframe) + svg.addClass(this.element, "sim-wireframe"); + + if (props && props.theme) + this.updateTheme(); + + if (props && props.runtime) { + this.board = this.props.runtime.board as pxsim.DalBoard; + this.board.updateSubscribers.push(() => this.updateState()); + this.updateState(); + this.attachEvents(); + } + } + + public getView(): SVGAndSize { + return { + el: this.element, + y: 0, + x: 0, + w: MB_WIDTH, + h: MB_HEIGHT + }; + } + + public getCoord(pinNm: string): Coord { + return this.pinNmToCoord[pinNm]; + } + + public highlightPin(pinNm: string): void { + //TODO: for instructions + } + + public getPinDist(): number { + return littlePinDist * 1.7; + } + + public recordPinCoords() { + const pinsY = 356.7 + 40; + pinNames.forEach((nm, i) => { + let x = pinMids[i]; + this.pinNmToCoord[nm] = [x, pinsY]; + }); } private updateTheme() { @@ -123,12 +340,14 @@ namespace pxsim.micro_bit { if (!state) return; let theme = this.props.theme; - state.buttons.forEach((btn, index) => { + let bpState = state.buttonPairState; + let buttons = [bpState.aBtn, bpState.bBtn, bpState.abBtn]; + buttons.forEach((btn, index) => { svg.fill(this.buttons[index], btn.pressed ? theme.buttonDown : theme.buttonUp); }); - let bw = state.displayMode == pxsim.DisplayMode.bw - let img = state.image; + let bw = state.ledMatrixState.displayMode == pxsim.DisplayMode.bw + let img = state.ledMatrixState.image; this.leds.forEach((led, i) => { let sel = (led) sel.style.opacity = ((bw ? img.data[i] > 0 ? 255 : 0 : img.data[i]) / 255.0) + ""; @@ -147,7 +366,7 @@ namespace pxsim.micro_bit { private updateGestures() { let state = this.board; - if (state.useShake && !this.shakeButton) { + if (state.accelerometerState.useShake && !this.shakeButton) { this.shakeButton = svg.child(this.g, "circle", { cx: 380, cy: 100, r: 16.5 }) as SVGCircleElement; svg.fill(this.shakeButton, this.props.theme.virtualButtonUp) this.shakeButton.addEventListener(pointerEvents.down, ev => { @@ -170,7 +389,7 @@ namespace pxsim.micro_bit { private updateButtonAB() { let state = this.board; - if (state.usesButtonAB && !this.buttonABText) { + if (state.buttonPairState.usesButtonAB && !this.buttonABText) { (this.buttonsOuter[2]).style.visibility = "visible"; (this.buttons[2]).style.visibility = "visible"; this.buttonABText = svg.child(this.g, "text", { class: "sim-text", x: 370, y: 272 }) as SVGTextElement; @@ -203,7 +422,7 @@ namespace pxsim.micro_bit { private updateTemperature() { let state = this.board; - if (!state || !state.usesTemperature) return; + if (!state || !state.thermometerState.usesTemperature) return; let tmin = -5; let tmax = 50; @@ -227,13 +446,13 @@ namespace pxsim.micro_bit { (ev) => { let cur = svg.cursorPoint(pt, this.element, ev); let t = Math.max(0, Math.min(1, (260 - cur.y) / 140)) - state.temperature = Math.floor(tmin + t * (tmax - tmin)); + state.thermometerState.temperature = Math.floor(tmin + t * (tmax - tmin)); this.updateTemperature(); }, ev => { }, ev => { }) } - let t = Math.max(tmin, Math.min(tmax, state.temperature)) - let per = Math.floor((state.temperature - tmin) / (tmax - tmin) * 100) + let t = Math.max(tmin, Math.min(tmax, state.thermometerState.temperature)) + let per = Math.floor((state.thermometerState.temperature - tmin) / (tmax - tmin) * 100) svg.setGradientValue(this.thermometerGradient, 100 - per + "%"); this.thermometerText.textContent = t + "°C"; } @@ -242,7 +461,7 @@ namespace pxsim.micro_bit { let xc = 258; let yc = 75; let state = this.board; - if (!state || !state.usesHeading) return; + if (!state || !state.compassState.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"); @@ -252,16 +471,16 @@ namespace pxsim.micro_bit { 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; + state.compassState.heading = Math.floor(Math.atan2(cur.y - yc, cur.x - xc) * 180 / Math.PI + 90); + if (state.compassState.heading < 0) state.compassState.heading += 360; this.updateHeading(); }); this.headInitialized = true; } - let txt = state.heading.toString() + "°"; + let txt = state.compassState.heading.toString() + "°"; if (txt != this.headText.textContent) { - svg.rotateElement(this.head, xc, yc, state.heading + 180); + svg.rotateElement(this.head, xc, yc, state.compassState.heading + 180); this.headText.textContent = txt; } } @@ -297,12 +516,12 @@ namespace pxsim.micro_bit { let state = this.board; if (!state) return; - state.pins.forEach((pin, i) => this.updatePin(pin, i)); + state.edgeConnectorState.pins.forEach((pin, i) => this.updatePin(pin, i)); } private updateLightLevel() { let state = this.board; - if (!state || !state.usesLightLevel) return; + if (!state || !state.lightSensorState.usesLightLevel) return; if (!this.lightLevelButton) { let gid = "gradient-light-level"; @@ -320,8 +539,8 @@ namespace pxsim.micro_bit { let pos = svg.cursorPoint(pt, this.element, ev); let rs = r / 2; let level = Math.max(0, Math.min(255, Math.floor((pos.y - (cy - rs)) / (2 * rs) * 255))); - if (level != this.board.lightLevel) { - this.board.lightLevel = level; + if (level != this.board.lightSensorState.lightLevel) { + this.board.lightSensorState.lightLevel = level; this.applyLightLevel(); } }, ev => { }, @@ -330,12 +549,12 @@ namespace pxsim.micro_bit { this.updateTheme(); } - svg.setGradientValue(this.lightLevelGradient, Math.min(100, Math.max(0, Math.floor(state.lightLevel * 100 / 255))) + '%') - this.lightLevelText.textContent = state.lightLevel.toString(); + svg.setGradientValue(this.lightLevelGradient, Math.min(100, Math.max(0, Math.floor(state.lightSensorState.lightLevel * 100 / 255))) + '%') + this.lightLevelText.textContent = state.lightSensorState.lightLevel.toString(); } private applyLightLevel() { - let lv = this.board.lightLevel; + let lv = this.board.lightSensorState.lightLevel; svg.setGradientValue(this.lightLevelGradient, Math.min(100, Math.max(0, Math.floor(lv * 100 / 255))) + '%') this.lightLevelText.textContent = lv.toString(); } @@ -343,10 +562,10 @@ namespace pxsim.micro_bit { private updateTilt() { if (this.props.disableTilt) return; let state = this.board; - if (!state || !state.accelerometer.isActive) return; + if (!state || !state.accelerometerState.accelerometer.isActive) return; - let x = state.accelerometer.getX(); - let y = state.accelerometer.getY(); + let x = state.accelerometerState.accelerometer.getX(); + let y = state.accelerometerState.accelerometer.getY(); let af = 8 / 1023; this.element.style.transform = "perspective(30em) rotateX(" + y * af + "deg) rotateY(" + x * af + "deg)" @@ -358,129 +577,18 @@ namespace pxsim.micro_bit { this.element = svg.elt("svg") svg.hydrate(this.element, { "version": "1.0", - "viewBox": "0 0 498 406", - "enable-background": "new 0 0 498 406", + "viewBox": `0 0 ${MB_WIDTH} ${MB_HEIGHT}`, "class": "sim", "x": "0px", - "y": "0px" + "y": "0px", + "width": MB_WIDTH + "px", + "height": MB_HEIGHT + "px", }); this.style = svg.child(this.element, "style", {}); - this.style.textContent = ` -svg.sim { - margin-bottom:1em; -} -svg.sim.grayscale { - -moz-filter: grayscale(1); - -webkit-filter: grayscale(1); - filter: grayscale(1); -} -.sim-button { - pointer-events: none; -} - -.sim-button-outer:hover { - stroke:grey; - stroke-width: 3px; -} -.sim-button-nut { - fill:#704A4A; - pointer-events:none; -} -.sim-button-nut:hover { - stroke:1px solid #704A4A; -} -.sim-pin: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-light-level-button { - stroke:#fff; - stroke-width: 3px; -} - -.sim-antenna { - stroke:#555; - stroke-width: 2px; -} - -.sim-text { - font-family:"Lucida Console", Monaco, monospace; - font-size:25px; - fill:#fff; - pointer-events: none; -} - -.sim-text-pin { - font-family:"Lucida Console", Monaco, monospace; - font-size:20px; - fill:#fff; - pointer-events: none; -} - -.sim-thermometer { - stroke:#aaa; - stroke-width: 3px; -} - -/* animations */ -.sim-theme-glow { - animation-name: sim-theme-glow-animation; - animation-timing-function: ease-in-out; - animation-direction: alternate; - animation-iteration-count: infinite; - animation-duration: 1.25s; -} -@keyframes sim-theme-glow-animation { - from { opacity: 1; } - to { opacity: 0.75; } -} - -.sim-flash { - animation-name: sim-flash-animation; - animation-duration: 0.1s; -} - -@keyframes sim-flash-animation { - from { fill: yellow; } - to { fill: default; } -} - -.sim-flash-stroke { - 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; } -} - - `; - + this.style.textContent = MB_STYLE; this.defs = svg.child(this.element, "defs", {}); - this.g = svg.elt("g"); + this.g = svg.elt("g"); this.element.appendChild(this.g); // filters @@ -517,7 +625,7 @@ svg.sim.grayscale { } // head - this.head = svg.child(this.g, "g", {}); + this.head = svg.child(this.g, "g", {class: "sim-head"}); svg.child(this.head, "circle", { cx: 258, cy: 75, r: 100, fill: "transparent" }) this.logos.push(svg.path(this.head, "sim-theme sim-theme-glow", "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 sim-theme-glow", "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")); @@ -530,38 +638,19 @@ svg.sim.grayscale { "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, pi) => svg.path(this.g, "sim-pin sim-pin-touch", p, `P${pi}, ANALOG IN`)); + ].map((p, pi) => svg.path(this.g, "sim-pin sim-pin-touch", p)); // P3 - 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", "P3, ANALOG IN, LED Col 1")); + 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")); - [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 => { + pins4onXs.forEach(x => { this.pins.push(svg.child(this.g, "rect", { x: x, y: 356.7, width: 10, height: 50, class: "sim-pin" })); }) - svg.title(this.pins[4], "P4, ANALOG IN, LED Col 2") - svg.title(this.pins[5], "P5, BUTTON A") - svg.title(this.pins[6], "P6, LED Col 9") - svg.title(this.pins[7], "P7, LED Col 8") - svg.title(this.pins[8], "P8") - svg.title(this.pins[9], "P9, LED Col 7") - svg.title(this.pins[10], "P10, ANALOG IN, LED Col 3") - svg.title(this.pins[11], "P11, BUTTON B") - svg.title(this.pins[12], "P12, RESERVED ACCESSIBILITY") - svg.title(this.pins[13], "P13, SPI - SCK") - svg.title(this.pins[14], "P14, SPI - MISO") - svg.title(this.pins[15], "P15, SPI - MOSI") - svg.title(this.pins[16], "P16, SPI - Chip Select") - svg.title(this.pins[17], "P17, +3v3") - svg.title(this.pins[18], "P18, +3v3") - svg.title(this.pins[19], "P19, I2C - SCL") - svg.title(this.pins[20], "P20, I2C - SDA") - svg.title(this.pins[21], "GND") - - 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", "GND")); - - 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", "+3v3")); - 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", "GND")); + 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")); + 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.pins.forEach((p, i) => svg.hydrate(p, {title: pinTitles[i]})); this.pinGradients = this.pins.map((pin, i) => { let gid = "gradient-pin-" + i @@ -571,6 +660,7 @@ svg.sim.grayscale { }) this.pinTexts = [67, 165, 275].map(x => svg.child(this.g, "text", { class: "sim-text-pin", x: x, y: 345 })); + this.buttonsOuter = []; this.buttons = []; const outerBtn = (left: number, top: number) => { @@ -578,7 +668,7 @@ svg.sim.grayscale { const btnw = 56.2; const btnn = 6; const btnnm = 10 - let btng = svg.child(this.g, "g"); + let btng = svg.child(this.g, "g", {class: "sim-button-group"}); this.buttonsOuter.push(btng); svg.child(btng, "rect", { class: "sim-button-outer", x: left, y: top, rx: btnr, ry: btnr, width: btnw, height: btnw }); svg.child(btng, "circle", { class: "sim-button-nut", cx: left + btnnm, cy: top + btnnm, r: btnn }); @@ -620,7 +710,7 @@ svg.sim.grayscale { let tiltDecayer = 0; this.element.addEventListener(pointerEvents.move, (ev: MouseEvent) => { let state = this.board; - if (!state.accelerometer.isActive) return; + if (!state.accelerometerState.accelerometer.isActive) return; if (tiltDecayer) { clearInterval(tiltDecayer); @@ -635,18 +725,18 @@ svg.sim.grayscale { let z2 = 1023 * 1023 - x * x - y * y; let z = Math.floor((z2 > 0 ? -1 : 1) * Math.sqrt(Math.abs(z2))); - state.accelerometer.update(x, y, z); + state.accelerometerState.accelerometer.update(x, y, z); this.updateTilt(); }, false); this.element.addEventListener(pointerEvents.leave, (ev: MouseEvent) => { let state = this.board; - if (!state.accelerometer.isActive) return; + if (!state.accelerometerState.accelerometer.isActive) return; if (!tiltDecayer) { tiltDecayer = setInterval(() => { - let accx = state.accelerometer.getX(MicroBitCoordinateSystem.RAW); + let accx = state.accelerometerState.accelerometer.getX(MicroBitCoordinateSystem.RAW); accx = Math.floor(Math.abs(accx) * 0.85) * (accx > 0 ? 1 : -1); - let accy = state.accelerometer.getY(MicroBitCoordinateSystem.RAW); + let accy = state.accelerometerState.accelerometer.getY(MicroBitCoordinateSystem.RAW); accy = Math.floor(Math.abs(accy) * 0.85) * (accy > 0 ? 1 : -1); let accz = -Math.sqrt(Math.max(0, 1023 * 1023 - accx * accx - accy * accy)); if (Math.abs(accx) <= 24 && Math.abs(accy) <= 24) { @@ -656,20 +746,20 @@ svg.sim.grayscale { accy = 0; accz = -1023; } - state.accelerometer.update(accx, accy, accz); + state.accelerometerState.accelerometer.update(accx, accy, accz); this.updateTilt(); }, 50) } }, false); this.pins.forEach((pin, index) => { - if (!this.board.pins[index]) return; + if (!this.board.edgeConnectorState.pins[index]) return; let pt = this.element.createSVGPoint(); svg.buttonEvents(pin, // move ev => { let state = this.board; - let pin = state.pins[index]; + let pin = state.edgeConnectorState.pins[index]; let svgpin = this.pins[index]; if (pin.mode & PinFlags.Input) { let cursor = svg.cursorPoint(pt, this.element, ev); @@ -681,7 +771,7 @@ svg.sim.grayscale { // start ev => { let state = this.board; - let pin = state.pins[index]; + let pin = state.edgeConnectorState.pins[index]; let svgpin = this.pins[index]; svg.addClass(svgpin, "touched"); if (pin.mode & PinFlags.Input) { @@ -694,7 +784,7 @@ svg.sim.grayscale { // stop (ev: MouseEvent) => { let state = this.board; - let pin = state.pins[index]; + let pin = state.edgeConnectorState.pins[index]; let svgpin = this.pins[index]; svg.removeClass(svgpin, "touched"); this.updatePin(pin, index); @@ -704,70 +794,73 @@ svg.sim.grayscale { this.pins.slice(0, 3).forEach((btn, index) => { btn.addEventListener(pointerEvents.down, ev => { let state = this.board; - state.pins[index].touched = true; - this.updatePin(state.pins[index], index); + state.edgeConnectorState.pins[index].touched = true; + this.updatePin(state.edgeConnectorState.pins[index], index); }) btn.addEventListener(pointerEvents.leave, ev => { let state = this.board; - state.pins[index].touched = false; - this.updatePin(state.pins[index], index); + state.edgeConnectorState.pins[index].touched = false; + this.updatePin(state.edgeConnectorState.pins[index], index); }) btn.addEventListener(pointerEvents.up, ev => { let state = this.board; - state.pins[index].touched = false; - this.updatePin(state.pins[index], index); - this.board.bus.queue(state.pins[index].id, DAL.MICROBIT_BUTTON_EVT_UP); - this.board.bus.queue(state.pins[index].id, DAL.MICROBIT_BUTTON_EVT_CLICK); + state.edgeConnectorState.pins[index].touched = false; + this.updatePin(state.edgeConnectorState.pins[index], index); + this.board.bus.queue(state.edgeConnectorState.pins[index].id, DAL.MICROBIT_BUTTON_EVT_UP); + this.board.bus.queue(state.edgeConnectorState.pins[index].id, DAL.MICROBIT_BUTTON_EVT_CLICK); }) }) + + let bpState = this.board.buttonPairState; + let stateButtons = [bpState.aBtn, bpState.bBtn, bpState.abBtn]; this.buttonsOuter.slice(0, 2).forEach((btn, index) => { btn.addEventListener(pointerEvents.down, ev => { let state = this.board; - state.buttons[index].pressed = true; + stateButtons[index].pressed = true; svg.fill(this.buttons[index], this.props.theme.buttonDown); }) btn.addEventListener(pointerEvents.leave, ev => { let state = this.board; - state.buttons[index].pressed = false; + stateButtons[index].pressed = false; svg.fill(this.buttons[index], this.props.theme.buttonUp); }) btn.addEventListener(pointerEvents.up, ev => { let state = this.board; - state.buttons[index].pressed = false; + stateButtons[index].pressed = false; svg.fill(this.buttons[index], this.props.theme.buttonUp); - this.board.bus.queue(state.buttons[index].id, DAL.MICROBIT_BUTTON_EVT_UP); - this.board.bus.queue(state.buttons[index].id, DAL.MICROBIT_BUTTON_EVT_CLICK); + this.board.bus.queue(stateButtons[index].id, DAL.MICROBIT_BUTTON_EVT_UP); + this.board.bus.queue(stateButtons[index].id, DAL.MICROBIT_BUTTON_EVT_CLICK); }) }) this.buttonsOuter[2].addEventListener(pointerEvents.down, ev => { let state = this.board; - state.buttons[0].pressed = true; - state.buttons[1].pressed = true; - state.buttons[2].pressed = true; + stateButtons[0].pressed = true; + stateButtons[1].pressed = true; + stateButtons[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(pointerEvents.leave, ev => { let state = this.board; - state.buttons[0].pressed = false; - state.buttons[1].pressed = false; - state.buttons[2].pressed = false; + stateButtons[0].pressed = false; + stateButtons[1].pressed = false; + stateButtons[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.virtualButtonUp); }) this.buttonsOuter[2].addEventListener(pointerEvents.up, ev => { let state = this.board; - state.buttons[0].pressed = false; - state.buttons[1].pressed = false; - state.buttons[2].pressed = false; + stateButtons[0].pressed = false; + stateButtons[1].pressed = false; + stateButtons[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.virtualButtonUp); - this.board.bus.queue(state.buttons[2].id, DAL.MICROBIT_BUTTON_EVT_UP); - this.board.bus.queue(state.buttons[2].id, DAL.MICROBIT_BUTTON_EVT_CLICK); + this.board.bus.queue(stateButtons[2].id, DAL.MICROBIT_BUTTON_EVT_UP); + this.board.bus.queue(stateButtons[2].id, DAL.MICROBIT_BUTTON_EVT_CLICK); }) } } diff --git a/sim/public/instructions.html b/sim/public/instructions.html new file mode 100644 index 00000000..723cf126 --- /dev/null +++ b/sim/public/instructions.html @@ -0,0 +1,203 @@ + + + + + + Assembly Instructions + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + +
+

+ + +
+ +
+
+
+
+ + \ No newline at end of file diff --git a/sim/simlib.ts b/sim/simlib.ts new file mode 100644 index 00000000..03276157 --- /dev/null +++ b/sim/simlib.ts @@ -0,0 +1,240 @@ +/// +/// +/// + +namespace pxsim { + export type BBRowCol = [/*row*/string, /*column*/string]; + export type BoardPin = string; + export interface BBLoc {type: "breadboard", rowCol: BBRowCol}; + export interface BoardLoc {type: "dalboard", pin: BoardPin}; + export type Loc = BBLoc | BoardLoc; + + export function initRuntimeWithDalBoard() { + U.assert(!runtime.board); + let b = new DalBoard(); + runtime.board = b; + } + if (!pxsim.initCurrentRuntime) { + pxsim.initCurrentRuntime = initRuntimeWithDalBoard; + } + + export function board() { + return runtime.board as DalBoard; + } + + export function mkRange(a: number, b: number): number[] { + let res: number[] = []; + for (; a < b; a++) + res.push(a); + return res; + } + + export function parseQueryString(): (key: string) => string { + let qs = window.location.search.substring(1); + let getQsVal = (key: string) => decodeURIComponent((qs.split(`${key}=`)[1] || "").split("&")[0] || "").replace(/\+/g, " "); + return getQsVal; + } +} + +namespace pxsim.visuals { + export interface IPointerEvents { + up: string, + down: string, + move: string, + leave: string + } + + export const pointerEvents: IPointerEvents = !!(window as any).PointerEvent ? { + up: "pointerup", + down: "pointerdown", + move: "pointermove", + leave: "pointerleave" + } : { + up: "mouseup", + down: "mousedown", + move: "mousemove", + leave: "mouseleave" + }; + + export function translateEl(el: SVGElement, xy: [number, number]) { + //TODO append translation instead of replacing the full transform + svg.hydrate(el, {transform: `translate(${xy[0]} ${xy[1]})`}); + } + + export interface ComposeOpts { + el1: SVGAndSize, + scaleUnit1: number, + el2: SVGAndSize, + scaleUnit2: number, + margin: [number, number, number, number], + middleMargin: number, + maxWidth?: string, + maxHeight?: string, + } + export interface ComposeResult { + host: SVGSVGElement, + scaleUnit: number, + under: SVGGElement, + over: SVGGElement, + edges: number[], + toHostCoord1: (xy: Coord) => Coord, + toHostCoord2: (xy: Coord) => Coord, + } + export function composeSVG(opts: ComposeOpts): ComposeResult { + let [a, b] = [opts.el1, opts.el2]; + U.assert(a.x == 0 && a.y == 0 && b.x == 0 && b.y == 0, "el1 and el2 x,y offsets not supported"); + let setXY = (e: SVGSVGElement, x: number, y: number) => svg.hydrate(e, {x: x, y: y}); + let setWH = (e: SVGSVGElement, w: string, h: string) => { + if (w) + svg.hydrate(e, {width: w}); + if (h) + svg.hydrate(e, {height: h}); + } + let setWHpx = (e: SVGSVGElement, w: number, h: number) => svg.hydrate(e, {width: `${w}px`, height: `${h}px`}); + let scaleUnit = opts.scaleUnit2; + let aScalar = opts.scaleUnit2 / opts.scaleUnit1; + let bScalar = 1.0; + let aw = a.w * aScalar; + let ah = a.h * aScalar; + setWHpx(a.el, aw, ah); + let bw = b.w * bScalar; + let bh = b.h * bScalar; + setWHpx(b.el, bw, bh); + let [mt, mr, mb, ml] = opts.margin; + let mm = opts.middleMargin; + let innerW = Math.max(aw, bw); + let ax = mr + (innerW - aw) / 2.0; + let ay = mt; + setXY(a.el, ax, ay); + let bx = mr + (innerW - bw) / 2.0; + let by = ay + ah + mm; + setXY(b.el, bx, by); + let edges = [ay, ay + ah, by, by + bh]; + let w = mr + innerW + ml; + let h = mt + ah + mm + bh + mb; + let host = svg.elt("svg", { + "version": "1.0", + "viewBox": `0 0 ${w} ${h}`, + "class": `sim-bb`, + }); + setWH(host, opts.maxWidth, opts.maxHeight); + setXY(host, 0, 0); + let under = svg.child(host, "g"); + host.appendChild(a.el); + host.appendChild(b.el); + let over = svg.child(host, "g"); + let toHostCoord1 = (xy: Coord): Coord => { + let [x, y] = xy; + return [x * aScalar + ax, y * aScalar + ay]; + }; + let toHostCoord2 = (xy: Coord): Coord => { + let [x, y] = xy; + return [x * bScalar + bx, y * bScalar + by]; + }; + return { + under: under, + over: over, + host: host, + edges: edges, + scaleUnit: scaleUnit, + toHostCoord1: toHostCoord1, + toHostCoord2: toHostCoord2, + }; + } + + export function mkScaleFn(originUnit: number, targetUnit: number): (n: number) => number { + return (n: number) => n * (targetUnit / originUnit); + } + export interface MkImageOpts { + image: string, + width: number, + height: number, + imageUnitDist: number, + targetUnitDist: number + } + export function mkImageSVG(opts: MkImageOpts): SVGAndSize { + let scaleFn = mkScaleFn(opts.imageUnitDist, opts.targetUnitDist); + let w = scaleFn(opts.width); + let h = scaleFn(opts.height); + let img = svg.elt("image", { + width: w, + height: h, + "href": `${opts.image}` + }); + return {el: img, w: w, h: h, x: 0, y: 0}; + } + + export type Coord = [number, number]; + export function findDistSqrd(a: Coord, b: Coord): number { + let x = a[0] - b[0]; + let y = a[1] - b[1]; + return x * x + y * y; + } + export function findClosestCoordIdx(a: Coord, bs: Coord[]): number { + let dists = bs.map(b => findDistSqrd(a, b)); + let minIdx = dists.reduce((prevIdx, currDist, currIdx, arr) => { + return currDist < arr[prevIdx] ? currIdx : prevIdx; + }, 0); + return minIdx; + } + + export interface IBoardComponent { + style: string, + element: SVGElement, + defs: SVGElement[], + init(bus: EventBus, state: T, svgEl: SVGSVGElement, gpioPins: string[], otherArgs: string[]): void, //NOTE: constructors not supported in interfaces + moveToCoord(xy: Coord): void, + updateState(): void, + updateTheme(): void, + } + + export function mkTxt(cx: number, cy: number, size: number, rot: number, txt: string, txtXOffFactor?: number, txtYOffFactor?: number): SVGTextElement { + let el = svg.elt("text") + //HACK: these constants (txtXOffFactor, txtYOffFactor) tweak the way this algorithm knows how to center the text + txtXOffFactor = txtXOffFactor || -0.33333; + txtYOffFactor = txtYOffFactor || 0.3; + const xOff = txtXOffFactor * size * txt.length; + const yOff = txtYOffFactor * size; + svg.hydrate(el, {style: `font-size:${size}px;`, + transform: `translate(${cx} ${cy}) rotate(${rot}) translate(${xOff} ${yOff})` }); + svg.addClass(el, "noselect"); + el.textContent = txt; + return el; + } + + export type WireColor = + "black" | "white" | "gray" | "purple" | "blue" | "green" | "yellow" | "orange" | "red" | "brown"; + export const WIRE_COLOR_MAP: Map = { + black: "#514f4d", + white: "#fcfdfc", + gray: "#acabab", + purple: "#a772a1", + blue: "#01a6e8", + green: "#3cce73", + yellow: "#ece600", + orange: "#fdb262", + red: "#f44f43", + brown: "#c89764", + } + export function mapWireColor(clr: WireColor | string): string { + return WIRE_COLOR_MAP[clr] || clr; + } + + export interface SVGAndSize { + el: T, + y: number, + x: number, + w: number, + h: number + }; + export type SVGElAndSize = SVGAndSize; + + export const PIN_DIST = 15; + + export interface BoardView { + getView(): SVGAndSize; + getCoord(pinNm: string): Coord; + getPinDist(): number; + highlightPin(pinNm: string): void; + } +} \ No newline at end of file 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 diff --git a/sim/visuals/boardhost.ts b/sim/visuals/boardhost.ts new file mode 100644 index 00000000..fdbffb50 --- /dev/null +++ b/sim/visuals/boardhost.ts @@ -0,0 +1,193 @@ +namespace pxsim.visuals { + export interface BoardHostOpts { + state: DalBoard, + boardDef: BoardDefinition, + cmpsList?: string[], + cmpDefs: Map, + fnArgs: any, + forceBreadboard?: boolean, + maxWidth?: string, + maxHeight?: string + wireframe?: boolean + } + export class BoardHost { + private components: IBoardComponent[] = []; + private wireFactory: WireFactory; + private breadboard: Breadboard; + private fromBBCoord: (xy: Coord) => Coord; + private fromMBCoord: (xy: Coord) => Coord; + private boardView: BoardView; + private view: SVGSVGElement; + private style: SVGStyleElement; + private defs: SVGDefsElement; + private state: DalBoard; + private useCrocClips: boolean; + + constructor(opts: BoardHostOpts) { + this.state = opts.state; + let onboardCmps = opts.boardDef.onboardComponents || []; + let activeComponents = (opts.cmpsList || []).filter(c => onboardCmps.indexOf(c) < 0); + activeComponents.sort(); + this.useCrocClips = opts.boardDef.useCrocClips; + + if (opts.boardDef.visual === "microbit") { + this.boardView = new visuals.MicrobitBoardSvg({ + runtime: runtime, + theme: visuals.randomTheme(), + disableTilt: false, + wireframe: opts.wireframe, + }); + } else { + let boardVis = opts.boardDef.visual as BoardImageDefinition; + this.boardView = new visuals.GenericBoardSvg({ + visualDef: boardVis, + wireframe: opts.wireframe, + }); + } + + let useBreadboard = 0 < activeComponents.length || opts.forceBreadboard; + if (useBreadboard) { + this.breadboard = new Breadboard({ + wireframe: opts.wireframe, + }); + let bMarg = opts.boardDef.marginWhenBreadboarding || [0, 0, 40, 0]; + let composition = composeSVG({ + el1: this.boardView.getView(), + scaleUnit1: this.boardView.getPinDist(), + el2: this.breadboard.getSVGAndSize(), + scaleUnit2: this.breadboard.getPinDist(), + margin: [bMarg[0], bMarg[1], 20, bMarg[3]], + middleMargin: bMarg[2], + maxWidth: opts.maxWidth, + maxHeight: opts.maxHeight, + }); + let under = composition.under; + let over = composition.over; + this.view = composition.host; + let edges = composition.edges; + this.fromMBCoord = composition.toHostCoord1; + this.fromBBCoord = composition.toHostCoord2; + let pinDist = composition.scaleUnit; + + this.style = svg.child(this.view, "style", {}); + this.defs = svg.child(this.view, "defs", {}); + + this.wireFactory = new WireFactory(under, over, edges, this.style, this.getLocCoord.bind(this)); + + let allocRes = allocateDefinitions({ + boardDef: opts.boardDef, + cmpDefs: opts.cmpDefs, + fnArgs: opts.fnArgs, + getBBCoord: this.breadboard.getCoord.bind(this.breadboard), + cmpList: activeComponents, + }); + + this.addAll(allocRes); + } else { + let el = this.boardView.getView().el; + this.view = el; + if (opts.maxWidth) + svg.hydrate(this.view, { width: opts.maxWidth }); + if (opts.maxHeight) + svg.hydrate(this.view, { height: opts.maxHeight }); + } + + this.state.updateSubscribers.push(() => this.updateState()); + } + + public highlightBoardPin(pinNm: string) { + this.boardView.highlightPin(pinNm); + } + + public highlightBreadboardPin(rowCol: BBRowCol) { + this.breadboard.highlightLoc(rowCol); + } + + public highlightWire(wire: Wire) { + //TODO: move to wiring.ts + //underboard wires + wire.wires.forEach(e => { + svg.addClass(e, "highlight"); + (e).style["visibility"] = "visible"; + }); + + //un greyed out + svg.addClass(wire.endG, "highlight"); + } + + public getView(): SVGElement { + return this.view; + } + + private updateState() { + this.components.forEach(c => c.updateState()); + } + + private getBBCoord(rowCol: BBRowCol) { + let bbCoord = this.breadboard.getCoord(rowCol); + return this.fromBBCoord(bbCoord); + } + private getPinCoord(pin: string) { + let boardCoord = this.boardView.getCoord(pin); + return this.fromMBCoord(boardCoord); + } + public getLocCoord(loc: Loc): Coord { + let coord: Coord; + if (loc.type === "breadboard") { + let rowCol = (loc).rowCol; + coord = this.getBBCoord(rowCol); + } else { + let pinNm = (loc).pin; + coord = this.getPinCoord(pinNm); + } + if (!coord) { + console.error("Unknown location: " + name) + return [0, 0]; + } + return coord; + } + + public addComponent(cmpDesc: CmpInst): IBoardComponent { + let cmp: IBoardComponent = null; + let colOffset = 0; + if (typeof cmpDesc.visual === "string") { + let builtinVisual = cmpDesc.visual as string; + let cnstr = builtinComponentSimVisual[builtinVisual]; + let stateFn = builtinComponentSimState[builtinVisual]; + cmp = cnstr(); + cmp.init(this.state.bus, stateFn(this.state), this.view, cmpDesc.microbitPins, cmpDesc.otherArgs); + } else { + let vis = cmpDesc.visual as PartVisualDefinition; + cmp = new GenericPart(vis); + colOffset = vis.extraColumnOffset || 0; + } + this.components.push(cmp); + this.view.appendChild(cmp.element); + if (cmp.defs) + cmp.defs.forEach(d => this.defs.appendChild(d)); + this.style.textContent += cmp.style || ""; + let rowCol = [`${cmpDesc.breadboardStartRow}`, `${colOffset + cmpDesc.breadboardStartColumn}`]; + let coord = this.getBBCoord(rowCol); + cmp.moveToCoord(coord); + let getCmpClass = (type: string) => `sim-${type}-cmp`; + let cls = getCmpClass(name); + svg.addClass(cmp.element, cls); + svg.addClass(cmp.element, "sim-cmp"); + cmp.updateTheme(); + cmp.updateState(); + return cmp; + } + public addWire(inst: WireInst): Wire { + return this.wireFactory.addWire(inst.start, inst.end, inst.color, this.useCrocClips); + } + public addAll(basicWiresAndCmpsAndWires: AllocatorResult) { + let {powerWires, components} = basicWiresAndCmpsAndWires; + powerWires.forEach(w => this.addWire(w)); + components.forEach((cAndWs, idx) => { + let {component, wires} = cAndWs; + wires.forEach(w => this.addWire(w)); + this.addComponent(component); + }); + } + } +} \ No newline at end of file diff --git a/sim/visuals/breadboard.ts b/sim/visuals/breadboard.ts new file mode 100644 index 00000000..f213cf39 --- /dev/null +++ b/sim/visuals/breadboard.ts @@ -0,0 +1,649 @@ +namespace pxsim.visuals { + // The distance between the center of two pins. This is the constant on which everything else is based. + const PIN_DIST = 15; + // CSS styling for the breadboard + const BLUE = "#1AA5D7"; + const RED = "#DD4BA0"; + const BREADBOARD_CSS = ` + /* bread board */ + .sim-bb-background { + fill:#E0E0E0; + } + .sim-bb-pin { + fill:#999; + } + .sim-bb-pin-hover { + visibility: hidden; + pointer-events: all; + stroke-width: ${PIN_DIST / 2}px; + stroke: transparent; + fill: #777; + } + .sim-bb-pin-hover:hover { + visibility: visible; + fill:#444; + } + .sim-bb-group-wire { + stroke: #999; + stroke-width: ${PIN_DIST / 4}px; + visibility: hidden; + } + .sim-bb-pin-group { + pointer-events: all; + } + .sim-bb-label, + .sim-bb-label-hover { + font-family:"Lucida Console", Monaco, monospace; + fill:#555; + pointer-events: all; + stroke-width: 0; + cursor: default; + } + .sim-bb-label-hover { + visibility: hidden; + fill:#000; + font-weight: bold; + } + .sim-bb-bar { + stroke-width: 0; + } + .sim-bb-blue { + fill:${BLUE}; + stroke:${BLUE} + } + .sim-bb-red { + fill:${RED}; + stroke:${RED}; + } + .sim-bb-pin-group:hover .sim-bb-pin-hover, + .sim-bb-pin-group:hover .sim-bb-group-wire, + .sim-bb-pin-group:hover .sim-bb-label-hover { + visibility: visible; + } + .sim-bb-pin-group:hover .sim-bb-label { + visibility: hidden; + } + /* outline mode */ + .sim-bb-outline .sim-bb-background { + stroke-width: ${PIN_DIST / 7}px; + fill: #FFF; + stroke: #000; + } + .sim-bb-outline .sim-bb-mid-channel { + fill: #FFF; + stroke: #888; + stroke-width: 1px; + } + /* grayed out */ + .grayed .sim-bb-red, + .grayed .sim-bb-blue { + fill: #BBB; + } + .grayed .sim-bb-pin { + fill: #BBB; + } + .grayed .sim-bb-label { + fill: #BBB; + } + .grayed .sim-bb-background { + stroke: #BBB; + } + .grayed .sim-bb-group-wire { + stroke: #DDD; + } + /* highlighted */ + .sim-bb-label.highlight { + visibility: hidden; + } + .sim-bb-label-hover.highlight { + visibility: visible; + } + .sim-bb-blue.highlight { + fill:${BLUE}; + } + .sim-bb-red.highlight { + fill:${RED}; + } + ` + // Pin rows and coluns + const MID_ROWS = 10; + const MID_ROW_GAPS = [4, 4]; + const MID_ROW_AND_GAPS = MID_ROWS + MID_ROW_GAPS.length; + const MID_COLS = 30; + const BAR_ROWS = 2; + const BAR_COLS = 25; + const POWER_ROWS = BAR_ROWS * 2; + const POWER_COLS = BAR_COLS * 2; + const BAR_COL_GAPS = [4, 9, 14, 19]; + const BAR_COL_AND_GAPS = BAR_COLS + BAR_COL_GAPS.length; + // Essential dimensions + const WIDTH = PIN_DIST * (MID_COLS + 3); + const HEIGHT = PIN_DIST * (MID_ROW_AND_GAPS + POWER_ROWS + 5.5); + const MID_RATIO = 2.0 / 3.0; + const BAR_RATIO = (1.0 - MID_RATIO) * 0.5; + const MID_HEIGHT = HEIGHT * MID_RATIO; + const BAR_HEIGHT = HEIGHT * BAR_RATIO; + // Pin grids + const MID_GRID_WIDTH = (MID_COLS - 1) * PIN_DIST; + const MID_GRID_HEIGHT = (MID_ROW_AND_GAPS - 1) * PIN_DIST; + const MID_GRID_X = (WIDTH - MID_GRID_WIDTH) / 2.0; + const MID_GRID_Y = BAR_HEIGHT + (MID_HEIGHT - MID_GRID_HEIGHT) / 2.0; + const BAR_GRID_HEIGHT = (BAR_ROWS - 1) * PIN_DIST; + const BAR_GRID_WIDTH = (BAR_COL_AND_GAPS - 1) * PIN_DIST; + const BAR_TOP_GRID_X = (WIDTH - BAR_GRID_WIDTH) / 2.0; + const BAR_TOP_GRID_Y = (BAR_HEIGHT - BAR_GRID_HEIGHT) / 2.0; + const BAR_BOT_GRID_X = BAR_TOP_GRID_X; + const BAR_BOT_GRID_Y = BAR_TOP_GRID_Y + BAR_HEIGHT + MID_HEIGHT; + // Individual pins + const PIN_HOVER_SCALAR = 1.3; + const PIN_WIDTH = PIN_DIST / 2.5; + const PIN_ROUNDING = PIN_DIST / 7.5; + // Labels + const PIN_LBL_SIZE = PIN_DIST * 0.7; + const PIN_LBL_HOVER_SCALAR = 1.3; + const PLUS_LBL_SIZE = PIN_DIST * 1.7; + const MINUS_LBL_SIZE = PIN_DIST * 2; + const POWER_LBL_OFFSET = PIN_DIST * 0.8; + const MINUS_LBL_EXTRA_OFFSET = PIN_DIST * 0.07; + const LBL_ROTATION = -90; + // Channels + const CHANNEL_HEIGHT = PIN_DIST * 1.0; + const SMALL_CHANNEL_HEIGHT = PIN_DIST * 0.05; + // Background + const BACKGROUND_ROUNDING = PIN_DIST * 0.3; + + export interface GridPin { + el: SVGElement, + hoverEl: SVGElement, + cx: number, + cy: number, + row: string, + col: string, + group?: string + }; + export interface GridOptions { + xOffset?: number, + yOffset?: number, + rowCount: number, + colCount: number, + rowStartIdx?: number, + colStartIdx?: number, + pinDist: number, + mkPin: () => SVGElAndSize, + mkHoverPin: () => SVGElAndSize, + getRowName: (rowIdx: number) => string, + getColName: (colIdx: number) => string, + getGroupName?: (rowIdx: number, colIdx: number) => string, + rowIdxsWithGap?: number[], + colIdxsWithGap?: number[], + }; + export interface GridResult { + g: SVGGElement, + allPins: GridPin[], + } + export function mkGrid(opts: GridOptions): GridResult { + let xOff = opts.xOffset || 0; + let yOff = opts.yOffset || 0; + let allPins: GridPin[] = []; + let grid = svg.elt("g"); + let colIdxOffset = opts.colStartIdx || 0; + let rowIdxOffset = opts.rowStartIdx || 0; + let copyArr = (arr: T[]): T[] => arr ? arr.slice(0, arr.length) : []; + let removeAll = (arr: T[], e: T): number => { + let res = 0; + let idx: number; + while (0 <= (idx = arr.indexOf(e))) { + arr.splice(idx, 1); + res += 1; + } + return res; + }; + let rowGaps = 0; + let rowIdxsWithGap = copyArr(opts.rowIdxsWithGap) + for (let i = 0; i < opts.rowCount; i++) { + let colGaps = 0; + let colIdxsWithGap = copyArr(opts.colIdxsWithGap) + let cy = yOff + i * opts.pinDist + rowGaps * opts.pinDist; + let rowIdx = i + rowIdxOffset; + for (let j = 0; j < opts.colCount; j++) { + let cx = xOff + j * opts.pinDist + colGaps * opts.pinDist; + let colIdx = j + colIdxOffset; + const addEl = (pin: SVGElAndSize) => { + let pinX = cx - pin.w * 0.5; + let pinY = cy - pin.h * 0.5; + svg.hydrate(pin.el, {x: pinX, y: pinY}); + grid.appendChild(pin.el); + return pin.el; + } + let el = addEl(opts.mkPin()); + let hoverEl = addEl(opts.mkHoverPin()); + let row = opts.getRowName(rowIdx); + let col = opts.getColName(colIdx); + let group = opts.getGroupName ? opts.getGroupName(rowIdx, colIdx) : null; + let gridPin: GridPin = {el: el, hoverEl: hoverEl, cx: cx, cy: cy, row: row, col: col, group: group}; + allPins.push(gridPin); + //column gaps + colGaps += removeAll(colIdxsWithGap, colIdx); + } + //row gaps + rowGaps += removeAll(rowIdxsWithGap, rowIdx); + } + return {g: grid, allPins: allPins}; + } + function mkBBPin(): SVGElAndSize { + let el = svg.elt("rect"); + let width = PIN_WIDTH; + svg.hydrate(el, { + class: "sim-bb-pin", + rx: PIN_ROUNDING, + ry: PIN_ROUNDING, + width: width, + height: width + }); + return {el: el, w: width, h: width, x: 0, y: 0}; + } + function mkBBHoverPin(): SVGElAndSize { + let el = svg.elt("rect"); + let width = PIN_WIDTH * PIN_HOVER_SCALAR; + svg.hydrate(el, { + class: "sim-bb-pin-hover", + rx: PIN_ROUNDING, + ry: PIN_ROUNDING, + width: width, + height: width, + }); + return {el: el, w: width, h: width, x: 0, y: 0}; + } + export interface GridLabel { + el: SVGTextElement, + hoverEl: SVGTextElement, + txt: string, + group?: string, + }; + function mkBBLabel(cx: number, cy: number, size: number, rotation: number, txt: string, group: string, extraClasses?: string[]): GridLabel { + //lbl + let el = mkTxt(cx, cy, size, rotation, txt); + svg.addClass(el, "sim-bb-label"); + if (extraClasses) + extraClasses.forEach(c => svg.addClass(el, c)); + + //hover lbl + let hoverEl = mkTxt(cx, cy, size * PIN_LBL_HOVER_SCALAR, rotation, txt); + svg.addClass(hoverEl, "sim-bb-label-hover"); + if (extraClasses) + extraClasses.forEach(c => svg.addClass(hoverEl, c)); + + let lbl = {el: el, hoverEl: hoverEl, txt: txt, group: group}; + return lbl; + } + interface BBBar { + el: SVGRectElement, + group?: string + }; + + export interface BreadboardOpts { + wireframe?: boolean, + } + export class Breadboard { + public bb: SVGSVGElement; + private styleEl: SVGStyleElement; + private defs: SVGDefsElement; + + //truth + private allPins: GridPin[] = []; + private allLabels: GridLabel[] = []; + private allPowerBars: BBBar[] = []; + //quick lookup caches + private rowColToPin: Map> = {}; + private rowColToLbls: Map> = {}; + + constructor(opts: BreadboardOpts) { + this.buildDom(); + + if (opts.wireframe) + svg.addClass(this.bb, "sim-bb-outline"); + } + + public updateLocation(x: number, y: number) { + svg.hydrate(this.bb, { + x: `${x}px`, + y: `${y}px`, + }); + } + + public getPin(row: string, col: string): GridPin { + let colToPin = this.rowColToPin[row]; + if (!colToPin) + return null; + let pin = colToPin[col]; + if (!pin) + return null; + return pin; + } + public getCoord(rowCol: BBRowCol): Coord { + let [row, col] = rowCol; + let pin = this.getPin(row, col); + if (!pin) + return null; + return [pin.cx, pin.cy]; + } + + public getPinDist() { + return PIN_DIST; + } + + private buildDom() { + this.bb = svg.elt("svg", { + "version": "1.0", + "viewBox": `0 0 ${WIDTH} ${HEIGHT}`, + "class": `sim-bb`, + "width": WIDTH + "px", + "height": HEIGHT + "px", + }); + this.styleEl = svg.child(this.bb, "style", {}); + this.styleEl.textContent += BREADBOARD_CSS; + this.defs = svg.child(this.bb, "defs", {}); + + //background + svg.child(this.bb, "rect", { class: "sim-bb-background", width: WIDTH, height: HEIGHT, rx: BACKGROUND_ROUNDING, ry: BACKGROUND_ROUNDING}); + + //mid channel + let channelGid = "sim-bb-channel-grad"; + let channelGrad = svg.elt("linearGradient") + svg.hydrate(channelGrad, { id: channelGid, x1: "0%", y1: "0%", x2: "0%", y2: "100%" }); + this.defs.appendChild(channelGrad); + let channelDark = "#AAA"; + let channelLight = "#CCC"; + let stop1 = svg.child(channelGrad, "stop", { offset: "0%", style: `stop-color: ${channelDark};` }) + let stop2 = svg.child(channelGrad, "stop", { offset: "20%", style: `stop-color: ${channelLight};` }) + let stop3 = svg.child(channelGrad, "stop", { offset: "80%", style: `stop-color: ${channelLight};` }) + let stop4 = svg.child(channelGrad, "stop", { offset: "100%", style: `stop-color: ${channelDark};` }) + + const mkChannel = (cy: number, h: number, cls?: string) => { + let channel = svg.child(this.bb, "rect", { class: `sim-bb-channel ${cls || ""}`, y: cy - h / 2, width: WIDTH, height: h}); + channel.setAttribute("fill", `url(#${channelGid})`); + return channel; + } + + mkChannel(BAR_HEIGHT + MID_HEIGHT / 2, CHANNEL_HEIGHT, "sim-bb-mid-channel"); + mkChannel(BAR_HEIGHT, SMALL_CHANNEL_HEIGHT); + mkChannel(BAR_HEIGHT + MID_HEIGHT, SMALL_CHANNEL_HEIGHT); + + //-----pins + const getMidTopOrBot = (rowIdx: number) => rowIdx < MID_ROWS / 2.0 ? "b" : "t"; + const getBarTopOrBot = (colIdx: number) => colIdx < POWER_COLS / 2.0 ? "b" : "t"; + const alphabet = "abcdefghij".split("").reverse(); + const getColName = (colIdx: number) => `${colIdx + 1}`; + const getMidRowName = (rowIdx: number) => alphabet[rowIdx]; + const getMidGroupName = (rowIdx: number, colIdx: number) => { + let botOrTop = getMidTopOrBot(rowIdx); + let colNm = getColName(colIdx); + return `${botOrTop}${colNm}`; + }; + const getBarRowName = (rowIdx: number) => rowIdx === 0 ? "-" : "+"; + const getBarGroupName = (rowIdx: number, colIdx: number) => { + let botOrTop = getBarTopOrBot(colIdx); + let rowName = getBarRowName(rowIdx); + return `${rowName}${botOrTop}`; + }; + + //mid grid + let midGridRes = mkGrid({ + xOffset: MID_GRID_X, + yOffset: MID_GRID_Y, + rowCount: MID_ROWS, + colCount: MID_COLS, + pinDist: PIN_DIST, + mkPin: mkBBPin, + mkHoverPin: mkBBHoverPin, + getRowName: getMidRowName, + getColName: getColName, + getGroupName: getMidGroupName, + rowIdxsWithGap: MID_ROW_GAPS, + }); + let midGridG = midGridRes.g; + this.allPins = this.allPins.concat(midGridRes.allPins); + + //bot bar + let botBarGridRes = mkGrid({ + xOffset: BAR_BOT_GRID_X, + yOffset: BAR_BOT_GRID_Y, + rowCount: BAR_ROWS, + colCount: BAR_COLS, + pinDist: PIN_DIST, + mkPin: mkBBPin, + mkHoverPin: mkBBHoverPin, + getRowName: getBarRowName, + getColName: getColName, + getGroupName: getBarGroupName, + colIdxsWithGap: BAR_COL_GAPS, + }); + let botBarGridG = botBarGridRes.g; + this.allPins = this.allPins.concat(botBarGridRes.allPins); + + //top bar + let topBarGridRes = mkGrid({ + xOffset: BAR_TOP_GRID_X, + yOffset: BAR_TOP_GRID_Y, + rowCount: BAR_ROWS, + colCount: BAR_COLS, + colStartIdx: BAR_COLS, + pinDist: PIN_DIST, + mkPin: mkBBPin, + mkHoverPin: mkBBHoverPin, + getRowName: getBarRowName, + getColName: getColName, + getGroupName: getBarGroupName, + colIdxsWithGap: BAR_COL_GAPS.map(g => g + BAR_COLS), + }); + let topBarGridG = topBarGridRes.g; + this.allPins = this.allPins.concat(topBarGridRes.allPins); + + //tooltip + this.allPins.forEach(pin => { + let {el, row, col, hoverEl} = pin + let title = `(${row},${col})`; + svg.hydrate(el, {title: title}); + svg.hydrate(hoverEl, {title: title}); + }) + + //catalog pins + this.allPins.forEach(pin => { + let colToPin = this.rowColToPin[pin.row]; + if (!colToPin) + colToPin = this.rowColToPin[pin.row] = {}; + colToPin[pin.col] = pin; + }) + + //-----labels + const mkBBLabelAtPin = (row: string, col: string, xOffset: number, yOffset: number, txt: string, group?: string): GridLabel => { + let size = PIN_LBL_SIZE; + let rotation = LBL_ROTATION; + let loc = this.getCoord([row, col]); + let [cx, cy] = loc; + let t = mkBBLabel(cx + xOffset, cy + yOffset, size, rotation, txt, group); + return t; + } + + //columns + for (let colIdx = 0; colIdx < MID_COLS; colIdx++) { + let colNm = getColName(colIdx); + //top + let rowTIdx = 0; + let rowTNm = getMidRowName(rowTIdx); + let groupT = getMidGroupName(rowTIdx, colIdx); + let lblT = mkBBLabelAtPin(rowTNm, colNm, 0, -PIN_DIST, colNm, groupT); + this.allLabels.push(lblT); + //bottom + let rowBIdx = MID_ROWS - 1; + let rowBNm = getMidRowName(rowBIdx); + let groupB = getMidGroupName(rowBIdx, colIdx); + let lblB = mkBBLabelAtPin(rowBNm, colNm, 0, +PIN_DIST, colNm, groupB); + this.allLabels.push(lblB); + } + //rows + for (let rowIdx = 0; rowIdx < MID_ROWS; rowIdx++) { + let rowNm = getMidRowName(rowIdx); + //top + let colTIdx = 0; + let colTNm = getColName(colTIdx); + let lblT = mkBBLabelAtPin(rowNm, colTNm, -PIN_DIST, 0, rowNm); + this.allLabels.push(lblT); + //top + let colBIdx = MID_COLS - 1; + let colBNm = getColName(colBIdx); + let lblB = mkBBLabelAtPin(rowNm, colBNm, +PIN_DIST, 0, rowNm); + this.allLabels.push(lblB); + } + + //+- labels + let botPowerLabels = [ + //BL + mkBBLabel(0 + POWER_LBL_OFFSET + MINUS_LBL_EXTRA_OFFSET, BAR_HEIGHT + MID_HEIGHT + POWER_LBL_OFFSET, MINUS_LBL_SIZE, LBL_ROTATION, `-`, getBarGroupName(0, 0), [`sim-bb-blue`]), + mkBBLabel(0 + POWER_LBL_OFFSET, BAR_HEIGHT + MID_HEIGHT + BAR_HEIGHT - POWER_LBL_OFFSET, PLUS_LBL_SIZE, LBL_ROTATION, `+`, getBarGroupName(1, 0), [`sim-bb-red`]), + //BR + mkBBLabel(WIDTH - POWER_LBL_OFFSET + MINUS_LBL_EXTRA_OFFSET, BAR_HEIGHT + MID_HEIGHT + POWER_LBL_OFFSET, MINUS_LBL_SIZE, LBL_ROTATION, `-`, getBarGroupName(0, BAR_COLS - 1), [`sim-bb-blue`]), + mkBBLabel(WIDTH - POWER_LBL_OFFSET, BAR_HEIGHT + MID_HEIGHT + BAR_HEIGHT - POWER_LBL_OFFSET, PLUS_LBL_SIZE, LBL_ROTATION, `+`, getBarGroupName(1, BAR_COLS - 1), [`sim-bb-red`]), + ]; + this.allLabels = this.allLabels.concat(botPowerLabels); + let topPowerLabels = [ + //TL + mkBBLabel(0 + POWER_LBL_OFFSET + MINUS_LBL_EXTRA_OFFSET, 0 + POWER_LBL_OFFSET, MINUS_LBL_SIZE, LBL_ROTATION, `-`, getBarGroupName(0, BAR_COLS), [`sim-bb-blue`]), + mkBBLabel(0 + POWER_LBL_OFFSET, BAR_HEIGHT - POWER_LBL_OFFSET, PLUS_LBL_SIZE, LBL_ROTATION, `+`, getBarGroupName(1, BAR_COLS), [`sim-bb-red`]), + //TR + mkBBLabel(WIDTH - POWER_LBL_OFFSET + MINUS_LBL_EXTRA_OFFSET, 0 + POWER_LBL_OFFSET, MINUS_LBL_SIZE, LBL_ROTATION, `-`, getBarGroupName(0, POWER_COLS - 1), [`sim-bb-blue`]), + mkBBLabel(WIDTH - POWER_LBL_OFFSET, BAR_HEIGHT - POWER_LBL_OFFSET, PLUS_LBL_SIZE, LBL_ROTATION, `+`, getBarGroupName(1, POWER_COLS - 1), [`sim-bb-red`]), + ]; + this.allLabels = this.allLabels.concat(topPowerLabels); + + //catalog lbls + let lblNmToLbls: Map = {}; + this.allLabels.forEach(lbl => { + let {el, txt} = lbl; + let lbls = lblNmToLbls[txt] = lblNmToLbls[txt] || [] + lbls.push(lbl); + }); + const isPowerPin = (pin: GridPin) => pin.row === "-" || pin.row === "+"; + this.allPins.forEach(pin => { + let {row, col, group} = pin; + let colToLbls = this.rowColToLbls[row] || (this.rowColToLbls[row] = {}); + let lbls = colToLbls[col] || (colToLbls[col] = []); + if (isPowerPin(pin)) { + //power pins + let isBot = Number(col) <= BAR_COLS; + if (isBot) + botPowerLabels.filter(l => l.group == pin.group).forEach(l => lbls.push(l)); + else + topPowerLabels.filter(l => l.group == pin.group).forEach(l => lbls.push(l)); + } else { + //mid pins + let rowLbls = lblNmToLbls[row]; + rowLbls.forEach(l => lbls.push(l)); + let colLbls = lblNmToLbls[col]; + colLbls.forEach(l => lbls.push(l)); + } + }) + + //-----blue & red lines + const lnLen = BAR_GRID_WIDTH + PIN_DIST * 1.5; + const lnThickness = PIN_DIST / 5.0; + const lnYOff = PIN_DIST * 0.6; + const lnXOff = (lnLen - BAR_GRID_WIDTH) / 2.0; + const mkPowerLine = (x: number, y: number, group: string, cls: string): BBBar => { + let ln = svg.elt("rect"); + svg.hydrate(ln, { + class: `sim-bb-bar ${cls}`, + x: x, + y: y - lnThickness / 2.0, + width: lnLen, + height: lnThickness}); + let bar: BBBar = {el: ln, group: group}; + return bar; + } + let barLines = [ + //top + mkPowerLine(BAR_BOT_GRID_X - lnXOff, BAR_BOT_GRID_Y - lnYOff, getBarGroupName(0, POWER_COLS - 1), "sim-bb-blue"), + mkPowerLine(BAR_BOT_GRID_X - lnXOff, BAR_BOT_GRID_Y + PIN_DIST + lnYOff, getBarGroupName(1, POWER_COLS - 1), "sim-bb-red"), + //bot + mkPowerLine(BAR_TOP_GRID_X - lnXOff, BAR_TOP_GRID_Y - lnYOff, getBarGroupName(0, 0), "sim-bb-blue"), + mkPowerLine(BAR_TOP_GRID_X - lnXOff, BAR_TOP_GRID_Y + PIN_DIST + lnYOff, getBarGroupName(1, 0), "sim-bb-red"), + ]; + this.allPowerBars = this.allPowerBars.concat(barLines); + //attach power bars + this.allPowerBars.forEach(b => this.bb.appendChild(b.el)); + + //-----electrically connected groups + //make groups + let allGrpNms = this.allPins.map(p => p.group).filter((g, i, a) => a.indexOf(g) == i); + let groups: SVGGElement[] = allGrpNms.map(grpNm => { + let g = svg.elt("g"); + return g; + }); + groups.forEach(g => svg.addClass(g, "sim-bb-pin-group")); + groups.forEach((g, i) => svg.addClass(g, `group-${allGrpNms[i]}`)); + let grpNmToGroup: Map = {}; + allGrpNms.forEach((g, i) => grpNmToGroup[g] = groups[i]); + //group pins and add connecting wire + let grpNmToPins: Map = {}; + this.allPins.forEach((p, i) => { + let g = p.group; + let pins = grpNmToPins[g] || (grpNmToPins[g] = []); + pins.push(p); + }); + //connecting wire + allGrpNms.forEach(grpNm => { + let pins = grpNmToPins[grpNm]; + let [xs, ys] = [pins.map(p => p.cx), pins.map(p => p.cy)]; + let minFn = (arr: number[]) => arr.reduce((a, b) => a < b ? a : b); + let maxFn = (arr: number[]) => arr.reduce((a, b) => a > b ? a : b); + let [minX, maxX, minY, maxY] = [minFn(xs), maxFn(xs), minFn(ys), maxFn(ys)]; + let wire = svg.elt("rect"); + let width = Math.max(maxX - minX, 0.0001/*rects with no width aren't displayed*/); + let height = Math.max(maxY - minY, 0.0001); + svg.hydrate(wire, {x: minX, y: minY, width: width, height: height}); + svg.addClass(wire, "sim-bb-group-wire") + let g = grpNmToGroup[grpNm]; + g.appendChild(wire); + }); + //group pins + this.allPins.forEach(p => { + let g = grpNmToGroup[p.group]; + g.appendChild(p.el); + g.appendChild(p.hoverEl); + }) + //group lbls + let miscLblGroup = svg.elt("g"); + svg.hydrate(miscLblGroup, {class: "sim-bb-group-misc"}); + groups.push(miscLblGroup); + this.allLabels.forEach(l => { + if (l.group) { + let g = grpNmToGroup[l.group]; + g.appendChild(l.el); + g.appendChild(l.hoverEl); + } else { + miscLblGroup.appendChild(l.el); + miscLblGroup.appendChild(l.hoverEl); + } + }) + + //attach to bb + groups.forEach(g => this.bb.appendChild(g)); //attach to breadboard + } + + public getSVGAndSize(): SVGAndSize { + return {el: this.bb, y: 0, x: 0, w: WIDTH, h: HEIGHT}; + } + + public highlightLoc(rowCol: BBRowCol) { + let [row, col] = rowCol; + let pin = this.rowColToPin[row][col]; + let {cx, cy} = pin; + let lbls = this.rowColToLbls[row][col]; + const highlightLbl = (lbl: GridLabel) => { + svg.addClass(lbl.el, "highlight"); + svg.addClass(lbl.hoverEl, "highlight"); + }; + lbls.forEach(highlightLbl); + } + } +} \ No newline at end of file diff --git a/sim/visuals/buttonpair.ts b/sim/visuals/buttonpair.ts new file mode 100644 index 00000000..9ae5e7f4 --- /dev/null +++ b/sim/visuals/buttonpair.ts @@ -0,0 +1,204 @@ +/// +/// +/// + +namespace pxsim.visuals { + export function mkBtnSvg(xy: Coord): SVGAndSize { + let [innerCls, outerCls] = ["sim-button", "sim-button-outer"]; + const tabSize = PIN_DIST / 2.5; + const pegR = PIN_DIST / 5; + const btnR = PIN_DIST * .8; + const pegMargin = PIN_DIST / 8; + const plateR = PIN_DIST / 12; + + const pegOffset = pegMargin + pegR; + let [x, y] = xy; + const left = x - tabSize / 2; + const top = y - tabSize / 2; + const plateH = 3 * PIN_DIST - tabSize; + const plateW = 2 * PIN_DIST + tabSize; + const plateL = left; + const plateT = top + tabSize; + const btnCX = plateL + plateW / 2; + const btnCY = plateT + plateH / 2; + + let btng = svg.elt("g"); + //tabs + const mkTab = (x: number, y: number) => { + svg.child(btng, "rect", { class: "sim-button-tab", x: x, y: y, width: tabSize, height: tabSize}) + } + mkTab(left, top); + mkTab(left + 2 * PIN_DIST, top); + mkTab(left, top + 3 * PIN_DIST); + mkTab(left + 2 * PIN_DIST, top + 3 * PIN_DIST); + + //plate + svg.child(btng, "rect", { class: outerCls, x: plateL, y: plateT, rx: plateR, ry: plateR, width: plateW, height: plateH }); + + //pegs + const mkPeg = (x: number, y: number) => { + svg.child(btng, "circle", { class: "sim-button-nut", cx: x, cy: y, r: pegR }); + } + mkPeg(plateL + pegOffset, plateT + pegOffset) + mkPeg(plateL + plateW - pegOffset, plateT + pegOffset) + mkPeg(plateL + pegOffset, plateT + plateH - pegOffset) + mkPeg(plateL + plateW - pegOffset, plateT + plateH - pegOffset) + + //inner btn + let innerBtn = svg.child(btng, "circle", { class: innerCls, cx: btnCX, cy: btnCY, r: btnR }); + + //return + return { el: btng, y: top, x: left, w: plateW, h: plateH + 2 * tabSize }; + } + export const BUTTON_PAIR_STYLE = ` + .sim-button { + pointer-events: none; + fill: #000; + } + .sim-button-outer:active ~ .sim-button, + .sim-button-virtual:active { + fill: #FFA500; + } + .sim-button-outer { + cursor: pointer; + fill: #979797; + } + .sim-button-outer:hover { + stroke:gray; + stroke-width: ${PIN_DIST / 5}px; + } + .sim-button-nut { + fill:#000; + pointer-events:none; + } + .sim-button-nut:hover { + stroke:${PIN_DIST / 15}px solid #704A4A; + } + .sim-button-tab { + fill:#FFF; + pointer-events:none; + } + .sim-button-virtual { + cursor: pointer; + fill: rgba(255, 255, 255, 0.6); + stroke: rgba(255, 255, 255, 1); + stroke-width: ${PIN_DIST / 5}px; + } + .sim-button-virtual:hover { + stroke: rgba(128, 128, 128, 1); + } + .sim-text-virtual { + fill: #000; + pointer-events:none; + } + `; + export class ButtonPairView implements IBoardComponent { + public element: SVGElement; + public defs: SVGElement[]; + public style = BUTTON_PAIR_STYLE; + private state: ButtonPairState; + private bus: EventBus; + private aBtn: SVGGElement; + private bBtn: SVGGElement; + private abBtn: SVGGElement; + + public init(bus: EventBus, state: ButtonPairState) { + this.state = state; + this.bus = bus; + this.defs = []; + this.element = this.mkBtns(); + this.updateState(); + this.attachEvents(); + } + + public moveToCoord(xy: Coord) { + let btnWidth = PIN_DIST * 3; + let [x, y] = xy; + translateEl(this.aBtn, [x, y]) + translateEl(this.bBtn, [x + btnWidth, y]) + translateEl(this.abBtn, [x + PIN_DIST * 1.5, y + PIN_DIST * 4]) + } + + public updateState() { + let stateBtns = [this.state.aBtn, this.state.bBtn, this.state.abBtn]; + let svgBtns = [this.aBtn, this.bBtn, this.abBtn]; + + if (this.state.usesButtonAB && this.abBtn.style.visibility != "visible") { + this.abBtn.style.visibility = "visible"; + } + } + + public updateTheme() {} + + private mkBtns() { + this.aBtn = mkBtnSvg([0, 0]).el; + this.bBtn = mkBtnSvg([0, 0]).el; + + const mkVirtualBtn = () => { + const numPins = 2; + const w = PIN_DIST * 2.8; + const offset = (w - (numPins * PIN_DIST)) / 2; + const corner = PIN_DIST / 2; + const cx = 0 - offset + w / 2; + const cy = cx; + const txtSize = PIN_DIST * 1.3; + const x = -offset; + const y = -offset; + const txtXOff = PIN_DIST / 7; + const txtYOff = PIN_DIST / 10; + + let btng = svg.elt("g"); + let btn = svg.child(btng, "rect", { class: "sim-button-virtual", x: x, y: y, rx: corner, ry: corner, width: w, height: w}); + let btnTxt = mkTxt(cx + txtXOff, cy + txtYOff, txtSize, 0, "A+B"); + svg.addClass(btnTxt, "sim-text") + svg.addClass(btnTxt, "sim-text-virtual"); + btng.appendChild(btnTxt); + + return btng; + } + + this.abBtn = mkVirtualBtn(); + this.abBtn.style.visibility = "hidden"; + + let el = svg.elt("g"); + svg.addClass(el, "sim-buttonpair") + el.appendChild(this.aBtn); + el.appendChild(this.bBtn); + el.appendChild(this.abBtn); + + return el; + } + + private attachEvents() { + let btnStates = [this.state.aBtn, this.state.bBtn]; + let btnSvgs = [this.aBtn, this.bBtn]; + btnSvgs.forEach((btn, index) => { + btn.addEventListener(pointerEvents.down, ev => { + btnStates[index].pressed = true; + }) + btn.addEventListener(pointerEvents.leave, ev => { + btnStates[index].pressed = false; + }) + btn.addEventListener(pointerEvents.up, ev => { + btnStates[index].pressed = false; + this.bus.queue(btnStates[index].id, DAL.MICROBIT_BUTTON_EVT_UP); + this.bus.queue(btnStates[index].id, DAL.MICROBIT_BUTTON_EVT_CLICK); + }) + }) + let updateBtns = (s: boolean) => { + btnStates.forEach(b => b.pressed = s) + }; + this.abBtn.addEventListener(pointerEvents.down, ev => { + updateBtns(true); + }) + this.abBtn.addEventListener(pointerEvents.leave, ev => { + updateBtns(false); + }) + this.abBtn.addEventListener(pointerEvents.up, ev => { + updateBtns(false); + this.bus.queue(this.state.abBtn.id, DAL.MICROBIT_BUTTON_EVT_UP); + this.bus.queue(this.state.abBtn.id, DAL.MICROBIT_BUTTON_EVT_CLICK); + }) + } + } +} \ No newline at end of file diff --git a/sim/visuals/genericboard.ts b/sim/visuals/genericboard.ts new file mode 100644 index 00000000..59437f70 --- /dev/null +++ b/sim/visuals/genericboard.ts @@ -0,0 +1,306 @@ +/// +/// +/// + +namespace pxsim.visuals { + export const BOARD_SYTLE = ` + .noselect { + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Chrome/Safari/Opera */ + -khtml-user-select: none; /* Konqueror */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; /* Non-prefixed version, currently + not supported by any browser */ + } + + .sim-board-pin { + fill:#999; + stroke:#000; + stroke-width:${PIN_DIST / 3.0}px; + } + .sim-board-pin-lbl { + fill: #333; + } + .gray-cover { + fill:#FFF; + opacity: 0.7; + stroke-width:0; + visibility: hidden; + } + .sim-board-pin-hover { + visibility: hidden; + pointer-events: all; + stroke-width:${PIN_DIST / 6.0}px; + } + .sim-board-pin-hover:hover { + visibility: visible; + } + .sim-board-pin-lbl { + visibility: hidden; + } + .sim-board-outline .sim-board-pin-lbl { + visibility: visible; + } + .sim-board-pin-lbl { + fill: #555; + } + .sim-board-pin-lbl-hover { + fill: red; + } + .sim-board-outline .sim-board-pin-lbl-hover { + fill: black; + } + .sim-board-pin-lbl, + .sim-board-pin-lbl-hover { + font-family:"Lucida Console", Monaco, monospace; + pointer-events: all; + stroke-width: 0; + } + .sim-board-pin-lbl-hover { + visibility: hidden; + } + .sim-board-outline .sim-board-pin-hover:hover + .sim-board-pin-lbl, + .sim-board-pin-lbl.highlight { + visibility: hidden; + } + .sim-board-outline .sim-board-pin-hover:hover + * + .sim-board-pin-lbl-hover, + .sim-board-pin-lbl-hover.highlight { + visibility: visible; + } + /* Graying out */ + .grayed .sim-board-pin-lbl:not(.highlight) { + fill: #AAA; + } + .grayed .sim-board-pin:not(.highlight) { + fill:#BBB; + stroke:#777; + } + .grayed .gray-cover { + visibility: inherit; + } + .grayed .sim-cmp:not(.notgrayed) { + opacity: 0.3; + } + /* Highlighting */ + .sim-board-pin-lbl.highlight { + fill: #000; + font-weight: bold; + } + .sim-board-pin.highlight { + fill:#999; + stroke:#000; + } + `; + const PIN_LBL_SIZE = PIN_DIST * 0.7; + const PIN_LBL_HOVER_SIZE = PIN_LBL_SIZE * 1.5; + const SQUARE_PIN_WIDTH = PIN_DIST * 0.66666; + const SQUARE_PIN_HOVER_WIDTH = PIN_DIST * 0.66666 + PIN_DIST / 3.0; + + export interface GenericBoardProps { + visualDef: BoardImageDefinition; + wireframe?: boolean; + } + + let nextBoardId = 0; + export class GenericBoardSvg implements BoardView { + private element: SVGSVGElement; + private style: SVGStyleElement; + private defs: SVGDefsElement; + private g: SVGGElement; + private background: SVGElement; + private width: number; + private height: number; + private id: number; + + // pins & labels + //(truth) + private allPins: GridPin[] = []; + private allLabels: GridLabel[] = []; + //(cache) + private pinNmToLbl: Map = {}; + private pinNmToPin: Map = {}; + + constructor(public props: GenericBoardProps) { + //TODO: handle wireframe mode + this.id = nextBoardId++; + let visDef = props.visualDef; + let imgHref = props.wireframe ? visDef.outlineImage : visDef.image; + let boardImgAndSize = mkImageSVG({ + image: imgHref, + width: visDef.width, + height: visDef.height, + imageUnitDist: visDef.pinDist, + targetUnitDist: PIN_DIST + }); + let scaleFn = mkScaleFn(visDef.pinDist, PIN_DIST); + this.width = boardImgAndSize.w; + this.height = boardImgAndSize.h; + let img = boardImgAndSize.el; + this.element = svg.elt("svg"); + svg.hydrate(this.element, { + "version": "1.0", + "viewBox": `0 0 ${this.width} ${this.height}`, + "class": `sim sim-board-id-${this.id}`, + "x": "0px", + "y": "0px" + }); + if (props.wireframe) + svg.addClass(this.element, "sim-board-outline") + this.style = svg.child(this.element, "style", {}); + this.style.textContent += BOARD_SYTLE; + this.defs = svg.child(this.element, "defs", {}); + this.g = svg.elt("g"); + this.element.appendChild(this.g); + + // main board + this.g.appendChild(img); + this.background = img; + svg.hydrate(img, { class: "sim-board" }); + let backgroundCover = this.mkGrayCover(0, 0, this.width, this.height); + this.g.appendChild(backgroundCover); + + // ----- pins + const mkSquarePin = (): SVGElAndSize => { + let el = svg.elt("rect"); + let width = SQUARE_PIN_WIDTH; + svg.hydrate(el, { + class: "sim-board-pin", + width: width, + height: width, + }); + return {el: el, w: width, h: width, x: 0, y: 0}; + } + const mkSquareHoverPin = (): SVGElAndSize => { + let el = svg.elt("rect"); + let width = SQUARE_PIN_HOVER_WIDTH; + svg.hydrate(el, { + class: "sim-board-pin-hover", + width: width, + height: width + }); + return {el: el, w: width, h: width, x: 0, y: 0}; + } + const mkPinBlockGrid = (pinBlock: PinBlockDefinition, blockIdx: number) => { + let xOffset = scaleFn(pinBlock.x) + PIN_DIST / 2.0; + let yOffset = scaleFn(pinBlock.y) + PIN_DIST / 2.0; + let rowCount = 1; + let colCount = pinBlock.labels.length; + let getColName = (colIdx: number) => pinBlock.labels[colIdx]; + let getRowName = () => `${blockIdx + 1}` + let getGroupName = () => pinBlock.labels.join(" "); + let gridRes = mkGrid({ + xOffset: xOffset, + yOffset: yOffset, + rowCount: rowCount, + colCount: colCount, + pinDist: PIN_DIST, + mkPin: mkSquarePin, + mkHoverPin: mkSquareHoverPin, + getRowName: getRowName, + getColName: getColName, + getGroupName: getGroupName, + }); + let pins = gridRes.allPins; + let pinsG = gridRes.g; + svg.addClass(gridRes.g, "sim-board-pin-group"); + return gridRes; + }; + let pinBlocks = visDef.pinBlocks.map(mkPinBlockGrid); + let pinToBlockDef: PinBlockDefinition[] = []; + pinBlocks.forEach((blk, blkIdx) => blk.allPins.forEach((p, pIdx) => { + this.allPins.push(p); + pinToBlockDef.push(visDef.pinBlocks[blkIdx]); + })); + //tooltip + this.allPins.forEach(p => { + let tooltip = p.col; + svg.hydrate(p.el, {title: tooltip}); + svg.hydrate(p.hoverEl, {title: tooltip}); + }); + //attach pins + this.allPins.forEach(p => { + this.g.appendChild(p.el); + this.g.appendChild(p.hoverEl); + }); + //catalog pins + this.allPins.forEach(p => { + this.pinNmToPin[p.col] = p; + }); + + // ----- labels + const mkLabelTxtEl = (pinX: number, pinY: number, size: number, txt: string, pos: "above" | "below"): SVGTextElement => { + //TODO: extract constants + let lblY: number; + let lblX: number; + + if (pos === "below") { + let lblLen = size * 0.25 * txt.length; + lblX = pinX; + lblY = pinY + 12 + lblLen; + } else { + let lblLen = size * 0.32 * txt.length; + lblX = pinX; + lblY = pinY - 11 - lblLen; + } + let el = mkTxt(lblX, lblY, size, -90, txt); + return el; + }; + const mkLabel = (pinX: number, pinY: number, txt: string, pos: "above" | "below"): GridLabel => { + let el = mkLabelTxtEl(pinX, pinY, PIN_LBL_SIZE, txt, pos); + svg.addClass(el, "sim-board-pin-lbl"); + let hoverEl = mkLabelTxtEl(pinX, pinY, PIN_LBL_HOVER_SIZE, txt, pos); + svg.addClass(hoverEl, "sim-board-pin-lbl-hover"); + let label: GridLabel = {el: el, hoverEl: hoverEl, txt: txt}; + return label; + } + this.allLabels = this.allPins.map((p, pIdx) => { + let blk = pinToBlockDef[pIdx]; + return mkLabel(p.cx, p.cy, p.col, blk.labelPosition); + }); + //attach labels + this.allLabels.forEach(l => { + this.g.appendChild(l.el); + this.g.appendChild(l.hoverEl); + }); + //catalog labels + this.allPins.forEach((pin, pinIdx) => { + let lbl = this.allLabels[pinIdx]; + this.pinNmToLbl[pin.col] = lbl; + }); + } + + public getCoord(pinNm: string): Coord { + let pin = this.pinNmToPin[pinNm]; + if (!pin) + return null; + return [pin.cx, pin.cy]; + } + + private mkGrayCover(x: number, y: number, w: number, h: number) { + let rect = svg.elt("rect"); + svg.hydrate(rect, {x: x, y: y, width: w, height: h, class: "gray-cover"}); + return rect; + } + + + public getView(): SVGAndSize { + return {el: this.element, w: this.width, h: this.height, x: 0, y: 0}; + } + + public getPinDist() { + return PIN_DIST; + } + + public highlightPin(pinNm: string) { + let lbl = this.pinNmToLbl[pinNm]; + let pin = this.pinNmToPin[pinNm]; + if (lbl && pin) { + svg.addClass(lbl.el, "highlight"); + svg.addClass(lbl.hoverEl, "highlight"); + svg.addClass(pin.el, "highlight"); + svg.addClass(pin.hoverEl, "highlight"); + } + } + } +} \ No newline at end of file diff --git a/sim/visuals/genericpart.ts b/sim/visuals/genericpart.ts new file mode 100644 index 00000000..e31eee72 --- /dev/null +++ b/sim/visuals/genericpart.ts @@ -0,0 +1,40 @@ + +namespace pxsim.visuals { + export function mkGenericPartSVG(partVisual: PartVisualDefinition): SVGAndSize { + let imgAndSize = mkImageSVG({ + image: partVisual.image, + width: partVisual.width, + height: partVisual.height, + imageUnitDist: partVisual.pinDist, + targetUnitDist: PIN_DIST + }); + return imgAndSize; + } + + export class GenericPart implements IBoardComponent { + public style: string = ""; + public element: SVGElement; + defs: SVGElement[] = []; + + constructor(partVisual: PartVisualDefinition) { + let imgAndSize = mkGenericPartSVG(partVisual); + let img = imgAndSize.el; + let scaleFn = mkScaleFn(partVisual.pinDist, PIN_DIST); + let [pinX, pinY] = partVisual.firstPin; + let left = -scaleFn(pinX); + let top = -scaleFn(pinY); + translateEl(img, [left, top]); // So that 0,0 is on the first pin + this.element = svg.elt("g"); + this.element.appendChild(img); + } + + moveToCoord(xy: Coord): void { + translateEl(this.element, xy); + } + + //unused + init(bus: EventBus, state: any, svgEl: SVGSVGElement, gpioPins: string[], otherArgs: string[]): void { } + updateState(): void { } + updateTheme(): void { } + } +} \ No newline at end of file diff --git a/sim/visuals/ledmatrix.ts b/sim/visuals/ledmatrix.ts new file mode 100644 index 00000000..971cbde8 --- /dev/null +++ b/sim/visuals/ledmatrix.ts @@ -0,0 +1,129 @@ +/// +/// +/// + +namespace pxsim.visuals { + export function mkLedMatrixSvg(xy: Coord, rows: number, cols: number): + {el: SVGGElement, y: number, x: number, w: number, h: number, leds: SVGElement[], ledsOuter: SVGElement[], background: SVGElement} { + let result: {el: SVGGElement, y: number, x: number, w: number, h: number, leds: SVGElement[], ledsOuter: SVGElement[], background: SVGElement} + = {el: null, y: 0, x: 0, w: 0, h: 0, leds: [], ledsOuter: [], background: null}; + result.el = svg.elt("g"); + let width = cols * PIN_DIST; + let height = rows * PIN_DIST; + let ledRad = Math.round(PIN_DIST * .35); + let spacing = PIN_DIST; + let padding = (spacing - 2 * ledRad) / 2.0; + let [x, y] = xy; + let left = x - (ledRad + padding); + let top = y - (ledRad + padding); + result.x = left; + result.y = top; + result.w = width; + result.h = height; + result.background = svg.child(result.el, "rect", {class: "sim-display", x: left, y: top, width: width, height: height}) + + // ledsOuter + result.leds = []; + result.ledsOuter = []; + let hoverRad = ledRad * 1.2; + for (let i = 0; i < rows; ++i) { + let y = top + ledRad + i * spacing + padding; + for (let j = 0; j < cols; ++j) { + let x = left + ledRad + j * spacing + padding; + result.ledsOuter.push(svg.child(result.el, "circle", { class: "sim-led-back", cx: x, cy: y, r: ledRad })); + result.leds.push(svg.child(result.el, "circle", { class: "sim-led", cx: x, cy: y, r: hoverRad, title: `(${j},${i})` })); + } + } + + //default theme + svg.fill(result.background, defaultLedMatrixTheme.background); + svg.fills(result.leds, defaultLedMatrixTheme.ledOn); + svg.fills(result.ledsOuter, defaultLedMatrixTheme.ledOff); + + //turn off LEDs + result.leds.forEach(l => (l).style.opacity = 0 + ""); + + return result; + } + + export interface ILedMatrixTheme { + background?: string; + ledOn?: string; + ledOff?: string; + } + export var defaultLedMatrixTheme: ILedMatrixTheme = { + background: "#000", + ledOn: "#ff5f5f", + ledOff: "#DDD", + }; + + export const LED_MATRIX_STYLE = ` + .sim-led-back:hover { + stroke:#a0a0a0; + stroke-width:3px; + } + .sim-led:hover { + stroke:#ff7f7f; + stroke-width:3px; + } + ` + + export class LedMatrixView implements IBoardComponent { + private background: SVGElement; + private ledsOuter: SVGElement[]; + private leds: SVGElement[]; + private state: LedMatrixState; + private bus: EventBus; + public element: SVGElement; + public defs: SVGElement[]; + private theme: ILedMatrixTheme; + + private DRAW_SIZE = 8; + private ACTIVE_SIZE = 5; + + public style = LED_MATRIX_STYLE; + + public init(bus: EventBus, state: LedMatrixState) { + this.bus = bus; + this.state = state; + this.theme = defaultLedMatrixTheme; + this.defs = []; + this.element = this.buildDom(); + } + + public moveToCoord(xy: Coord) { + translateEl(this.element, xy); + } + + public updateTheme() { + svg.fill(this.background, this.theme.background); + svg.fills(this.leds, this.theme.ledOn); + svg.fills(this.ledsOuter, this.theme.ledOff); + } + + public updateState() { + let bw = this.state.displayMode == pxsim.DisplayMode.bw + let img = this.state.image; + this.leds.forEach((led, i) => { + let sel = (led) + let dx = i % this.DRAW_SIZE; + let dy = (i - dx) / this.DRAW_SIZE; + if (dx < this.ACTIVE_SIZE && dy < this.ACTIVE_SIZE) { + let j = dx + dy * this.ACTIVE_SIZE; + sel.style.opacity = ((bw ? img.data[j] > 0 ? 255 : 0 : img.data[j]) / 255.0) + ""; + } else { + sel.style.opacity = 0 + ""; + } + }) + } + + public buildDom() { + let res = mkLedMatrixSvg([0, 0], this.DRAW_SIZE, this.DRAW_SIZE); + let display = res.el; + this.background = res.background; + this.leds = res.leds; + this.ledsOuter = res.ledsOuter; + return display; + } + } +} \ No newline at end of file diff --git a/sim/visuals/neopixel.ts b/sim/visuals/neopixel.ts new file mode 100644 index 00000000..11116166 --- /dev/null +++ b/sim/visuals/neopixel.ts @@ -0,0 +1,256 @@ +/// +/// +/// +/// +/// +/// + +//TODO move to utils +namespace pxsim.visuals { + //expects rgb from 0,255, gives h in [0,360], s in [0, 100], l in [0, 100] + export function rgbToHsl(rgb: [number, number, number]): [number, number, number] { + let [r, g, b] = rgb; + let [r$, g$, b$] = [r / 255, g / 255, b / 255]; + let cMin = Math.min(r$, g$, b$); + let cMax = Math.max(r$, g$, b$); + let cDelta = cMax - cMin; + let h: number, s: number, l: number; + let maxAndMin = cMax + cMin; + + //lum + l = (maxAndMin / 2) * 100 + + if (cDelta === 0) + s = h = 0; + else { + //hue + if (cMax === r$) + h = 60 * (((g$ - b$) / cDelta) % 6); + else if (cMax === g$) + h = 60 * (((b$ - r$) / cDelta) + 2); + else if (cMax === b$) + h = 60 * (((r$ - g$) / cDelta) + 4); + + //sat + if (l > 50) + s = 100 * (cDelta / (2 - maxAndMin)); + else + s = 100 * (cDelta / maxAndMin); + } + + return [Math.floor(h), Math.floor(s), Math.floor(l)]; + } +} + +namespace pxsim.visuals { + const PIXEL_SPACING = PIN_DIST * 3; + const PIXEL_RADIUS = PIN_DIST; + const CANVAS_WIDTH = 1.2 * PIN_DIST; + const CANVAS_HEIGHT = 12 * PIN_DIST; + const CANVAS_VIEW_WIDTH = CANVAS_WIDTH; + const CANVAS_VIEW_HEIGHT = CANVAS_HEIGHT; + const CANVAS_VIEW_PADDING = PIN_DIST * 4; + const CANVAS_LEFT = 1.4 * PIN_DIST; + const CANVAS_TOP = PIN_DIST; + + // For the instructions parts list + export function mkNeoPixelPart(xy: Coord = [0, 0]): SVGElAndSize { + const NP_PART_XOFF = -13.5; + const NP_PART_YOFF = -11; + const NP_PART_WIDTH = 87.5; + const NP_PART_HEIGHT = 190; + const NEOPIXEL_PART_IMG = "neopixel.svg"; + let [x, y] = xy; + let l = x + NP_PART_XOFF; + let t = y + NP_PART_YOFF; + let w = NP_PART_WIDTH; + let h = NP_PART_HEIGHT; + let img = svg.elt("image"); + svg.hydrate(img, {class: "sim-neopixel-strip", x: l, y: t, width: w, height: h, + href: `/static/hardware/${NEOPIXEL_PART_IMG}`}); + return {el: img, x: l, y: t, w: w, h: h}; + } + export class NeoPixel implements SVGAndSize { + public el: SVGCircleElement; + public w: number; + public h: number; + public x: number; + public y: number; + public cx: number; + public cy: number; + + constructor(xy: Coord = [0, 0]) { + let circle = svg.elt("circle"); + let r = PIXEL_RADIUS; + let [cx, cy] = xy; + svg.hydrate(circle, {cx: cx, cy: cy, r: r, class: "sim-neopixel"}); + this.el = circle; + this.w = r * 2; + this.h = r * 2; + this.x = cx - r; + this.y = cy - r; + this.cx = cx; + this.cy = cy; + } + + public setRgb(rgb: [number, number, number]) { + let hsl = rgbToHsl(rgb); + let [h, s, l] = hsl; + //We ignore luminosity since it doesn't map well to real-life brightness + let fill = `hsl(${h}, ${s}%, 70%)`; + this.el.setAttribute("fill", fill); + } + } + + export class NeoPixelCanvas { + public canvas: SVGSVGElement; + public pin: number; + public pixels: NeoPixel[]; + private viewBox: [number, number, number, number]; + private background: SVGRectElement; + + constructor(pin: number) { + this.pixels = []; + this.pin = pin; + let el = svg.elt("svg"); + svg.hydrate(el, { + "class": `sim-neopixel-canvas`, + "x": "0px", + "y": "0px", + "width": `${CANVAS_WIDTH}px`, + "height": `${CANVAS_HEIGHT}px`, + }); + this.canvas = el; + this.background = svg.child(el, "rect", { class: "sim-neopixel-background hidden"}); + this.updateViewBox(-CANVAS_VIEW_WIDTH / 2, 0, CANVAS_VIEW_WIDTH, CANVAS_VIEW_HEIGHT); + } + + private updateViewBox(x: number, y: number, w: number, h: number) { + this.viewBox = [x, y, w, h]; + svg.hydrate(this.canvas, {"viewBox": `${x} ${y} ${w} ${h}`}); + svg.hydrate(this.background, {"x": x, "y": y, "width": w, "height": h}); + } + + public update(colors: RGBW[]) { + if (!colors || colors.length <= 0) + return; + + for (let i = 0; i < colors.length; i++) { + let pixel = this.pixels[i]; + if (!pixel) { + let cxy: Coord = [0, CANVAS_VIEW_PADDING + i * PIXEL_SPACING]; + pixel = this.pixels[i] = new NeoPixel(cxy); + this.canvas.appendChild(pixel.el); + } + let color = colors[i]; + pixel.setRgb(color); + svg.hydrate(pixel.el, {title: `offset: ${i}`}); + } + + //show the canvas if it's hidden + svg.removeClass(this.background, "hidden"); + + //resize if necessary + let [first, last] = [this.pixels[0], this.pixels[this.pixels.length - 1]] + let yDiff = last.cy - first.cy; + let newH = yDiff + CANVAS_VIEW_PADDING * 2; + let [oldX, oldY, oldW, oldH] = this.viewBox; + if (oldH < newH) { + let scalar = newH / oldH; + let newW = oldW * scalar; + this.updateViewBox(-newW / 2, oldY, newW, newH); + } + } + + public setLoc(xy: Coord) { + let [x, y] = xy; + svg.hydrate(this.canvas, {x: x, y: y}); + } + }; + + function gpioPinToPinNumber(gpioPin: string): number { + let pinNumStr = gpioPin.split("P")[1]; + let pinNum = Number(pinNumStr) + 7 /*MICROBIT_ID_IO_P0; TODO: don't hardcode this, import enums.d.ts*/; + return pinNum + } + function parseNeoPixelMode(modeStr: string): NeoPixelMode { + const modeMap: Map = { + "NeoPixelMode.RGB": NeoPixelMode.RGB, + "NeoPixelMode.RGBW": NeoPixelMode.RGBW, + "*": NeoPixelMode.RGB, + }; + let mode: NeoPixelMode = null; + for (let key in modeMap) { + if (key == modeStr) { + mode = modeMap[key]; + break; + } + } + U.assert(mode != null, "Unknown NeoPixelMode: " + modeStr); + return mode; + } + + export class NeoPixelView implements IBoardComponent { + public style: string = ` + .sim-neopixel-canvas { + } + .sim-neopixel-canvas-parent:hover { + transform-origin: center; + transform: scale(4) translateY(-60px); + } + .sim-neopixel-canvas .hidden { + visibility:hidden; + } + .sim-neopixel-background { + fill: rgba(255,255,255,0.9); + } + .sim-neopixel-strip { + } + `; + public element: SVGElement; + public defs: SVGElement[]; + private state: NeoPixelState; + private canvas: NeoPixelCanvas; + private part: SVGElAndSize; + private stripGroup: SVGGElement; + private lastLocation: Coord; + private pin: number; + private mode: NeoPixelMode; + + public init(bus: EventBus, state: NeoPixelState, svgEl: SVGSVGElement, gpioPins: string[], otherArgs: string[]): void { + U.assert(otherArgs.length === 1, "NeoPixels assumes a RGB vs RGBW mode is passed to it"); + let modeStr = otherArgs[0]; + this.mode = parseNeoPixelMode(modeStr); + this.state = state; + this.stripGroup = svg.elt("g"); + this.element = this.stripGroup; + let pinStr = gpioPins[0]; + this.pin = gpioPinToPinNumber(pinStr); + this.lastLocation = [0, 0]; + let part = mkNeoPixelPart(); + this.part = part; + this.stripGroup.appendChild(part.el); + let canvas = new NeoPixelCanvas(this.pin); + this.canvas = canvas; + let canvasG = svg.child(this.stripGroup, "g", {class: "sim-neopixel-canvas-parent"}); + canvasG.appendChild(canvas.canvas); + this.updateStripLoc(); + } + public moveToCoord(xy: Coord): void { + let [x, y] = xy; + let loc: Coord = [x, y]; + this.lastLocation = loc; + this.updateStripLoc(); + } + private updateStripLoc() { + let [x, y] = this.lastLocation; + this.canvas.setLoc([x + CANVAS_LEFT, y + CANVAS_TOP]); + svg.hydrate(this.part.el, {transform: `translate(${x} ${y})`}); //TODO: update part's l,h, etc. + } + public updateState(): void { + let colors = this.state.getColors(this.pin, this.mode); + this.canvas.update(colors); + } + public updateTheme (): void { } + } +} \ No newline at end of file diff --git a/sim/visuals/wiring.ts b/sim/visuals/wiring.ts new file mode 100644 index 00000000..1f1248e0 --- /dev/null +++ b/sim/visuals/wiring.ts @@ -0,0 +1,425 @@ +namespace pxsim.visuals { + const WIRE_WIDTH = PIN_DIST / 2.5; + const BB_WIRE_SMOOTH = 0.7; + const INSTR_WIRE_SMOOTH = 0.8; + const WIRE_PART_CURVE_OFF = 15; + const WIRE_PART_LENGTH = 100; + export const WIRES_CSS = ` + .sim-bb-wire { + fill:none; + stroke-linecap: round; + stroke-width:${WIRE_WIDTH}px; + pointer-events: none; + } + .sim-bb-wire-end { + stroke:#333; + fill:#333; + } + .sim-bb-wire-bare-end { + fill: #ccc; + } + .sim-bb-wire-hover { + stroke-width: ${WIRE_WIDTH}px; + visibility: hidden; + stroke-dasharray: ${PIN_DIST / 10.0},${PIN_DIST / 1.5}; + /*stroke-opacity: 0.4;*/ + } + .grayed .sim-bb-wire-ends-g:not(.highlight) .sim-bb-wire-end { + stroke: #777; + fill: #777; + } + .grayed .sim-bb-wire:not(.highlight) { + stroke: #CCC; + } + .sim-bb-wire-ends-g:hover .sim-bb-wire-end { + stroke: red; + fill: red; + } + .sim-bb-wire-ends-g:hover .sim-bb-wire-bare-end { + stroke: #FFF; + fill: #FFF; + } + `; + + export interface Wire { + endG: SVGGElement; + end1: SVGElement; + end2: SVGElement; + wires: SVGElement[]; + } + + function cssEncodeColor(color: string): string { + //HACK/TODO: do real CSS encoding. + return color + .replace(/\#/g, "-") + .replace(/\(/g, "-") + .replace(/\)/g, "-") + .replace(/\,/g, "-") + .replace(/\./g, "-") + .replace(/\s/g, ""); + } + export enum WireEndStyle { + BBJumper, + OpenJumper, + Croc, + } + export interface WireOpts { //TODO: use throughout + color?: string, + colorClass?: string, + bendFactor?: number, + } + export function mkWirePart(cp: [number, number], clr: string, croc: boolean = false): visuals.SVGAndSize { + let g = svg.elt("g"); + let [cx, cy] = cp; + let offset = WIRE_PART_CURVE_OFF; + let p1: visuals.Coord = [cx - offset, cy - WIRE_PART_LENGTH / 2]; + let p2: visuals.Coord = [cx + offset, cy + WIRE_PART_LENGTH / 2]; + clr = visuals.mapWireColor(clr); + let e1: SVGElAndSize; + if (croc) + e1 = mkCrocEnd(p1, true, clr); + else + e1 = mkOpenJumperEnd(p1, true, clr); + let s = mkWirePartSeg(p1, p2, clr); + let e2 = mkOpenJumperEnd(p2, false, clr); + g.appendChild(s.el); + g.appendChild(e1.el); + g.appendChild(e2.el); + let l = Math.min(e1.x, e2.x); + let r = Math.max(e1.x + e1.w, e2.x + e2.w); + let t = Math.min(e1.y, e2.y); + let b = Math.max(e1.y + e1.h, e2.y + e2.h); + return {el: g, x: l, y: t, w: r - l, h: b - t}; + } + function mkCurvedWireSeg(p1: [number, number], p2: [number, number], smooth: number, clrClass: string): SVGPathElement { + const coordStr = (xy: [number, number]): string => {return `${xy[0]}, ${xy[1]}`}; + let [x1, y1] = p1; + let [x2, y2] = p2 + let yLen = (y2 - y1); + let c1: [number, number] = [x1, y1 + yLen * smooth]; + let c2: [number, number] = [x2, y2 - yLen * smooth]; + let w = svg.mkPath("sim-bb-wire", `M${coordStr(p1)} C${coordStr(c1)} ${coordStr(c2)} ${coordStr(p2)}`); + svg.addClass(w, `wire-stroke-${clrClass}`); + return w; + } + function mkWirePartSeg(p1: [number, number], p2: [number, number], clr: string): visuals.SVGAndSize { + //TODO: merge with mkCurvedWireSeg + const coordStr = (xy: [number, number]): string => {return `${xy[0]}, ${xy[1]}`}; + let [x1, y1] = p1; + let [x2, y2] = p2 + let yLen = (y2 - y1); + let c1: [number, number] = [x1, y1 + yLen * .8]; + let c2: [number, number] = [x2, y2 - yLen * .8]; + let e = svg.mkPath("sim-bb-wire", `M${coordStr(p1)} C${coordStr(c1)} ${coordStr(c2)} ${coordStr(p2)}`); + (e).style["stroke"] = clr; + return {el: e, x: Math.min(x1, x2), y: Math.min(y1, y2), w: Math.abs(x1 - x2), h: Math.abs(y1 - y2)}; + } + function mkWireSeg(p1: [number, number], p2: [number, number], clrClass: string): SVGPathElement { + const coordStr = (xy: [number, number]): string => {return `${xy[0]}, ${xy[1]}`}; + let w = svg.mkPath("sim-bb-wire", `M${coordStr(p1)} L${coordStr(p2)}`); + svg.addClass(w, `wire-stroke-${clrClass}`); + return w; + } + function mkBBJumperEnd(p: [number, number], clrClass: string): SVGElement { + const endW = PIN_DIST / 4; + let w = svg.elt("circle"); + let x = p[0]; + let y = p[1]; + let r = WIRE_WIDTH / 2 + endW / 2; + svg.hydrate(w, {cx: x, cy: y, r: r, class: "sim-bb-wire-end"}); + svg.addClass(w, `wire-fill-${clrClass}`); + (w).style["stroke-width"] = `${endW}px`; + return w; + } + function mkOpenJumperEnd(p: [number, number], top: boolean, clr: string): visuals.SVGElAndSize { + let k = visuals.PIN_DIST * 0.24; + let plasticLength = k * 10; + let plasticWidth = k * 2; + let metalLength = k * 6; + let metalWidth = k; + const strokeWidth = visuals.PIN_DIST / 4.0; + let [cx, cy] = p; + let o = top ? -1 : 1; + let g = svg.elt("g") + + let el = svg.elt("rect"); + let h1 = plasticLength; + let w1 = plasticWidth; + let x1 = cx - w1 / 2; + let y1 = cy - (h1 / 2); + svg.hydrate(el, {x: x1, y: y1, width: w1, height: h1, rx: 0.5, ry: 0.5, class: "sim-bb-wire-end"}); + (el).style["stroke-width"] = `${strokeWidth}px`; + + let el2 = svg.elt("rect"); + let h2 = metalLength; + let w2 = metalWidth; + let cy2 = cy + o * (h1 / 2 + h2 / 2); + let x2 = cx - w2 / 2; + let y2 = cy2 - (h2 / 2); + svg.hydrate(el2, {x: x2, y: y2, width: w2, height: h2, class: "sim-bb-wire-bare-end"}); + (el2).style["fill"] = `#bbb`; + + g.appendChild(el2); + g.appendChild(el); + return {el: g, x: x1 - strokeWidth, y: Math.min(y1, y2), w: w1 + strokeWidth * 2, h: h1 + h2}; + } + function mkCrocEnd(p: [number, number], top: boolean, clr: string): SVGElAndSize { + //TODO: merge with mkOpenJumperEnd() + let k = visuals.PIN_DIST * 0.24; + const plasticWidth = k * 4; + const plasticLength = k * 10.0; + const metalWidth = k * 3.5; + const metalHeight = k * 3.5; + const pointScalar = .15; + const baseScalar = .3; + const taperScalar = .7; + const strokeWidth = visuals.PIN_DIST / 4.0; + let [cx, cy] = p; + let o = top ? -1 : 1; + let g = svg.elt("g") + + let el = svg.elt("polygon"); + let h1 = plasticLength; + let w1 = plasticWidth; + let x1 = cx - w1 / 2; + let y1 = cy - (h1 / 2); + let mkPnt = (xy: Coord) => `${xy[0]},${xy[1]}`; + let mkPnts = (...xys: Coord[]) => xys.map(xy => mkPnt(xy)).join(" "); + const topScalar = top ? pointScalar : baseScalar; + const midScalar = top ? taperScalar : (1 - taperScalar); + const botScalar = top ? baseScalar : pointScalar; + svg.hydrate(el, { + points: mkPnts( + [x1 + w1 * topScalar, y1], //TL + [x1 + w1 * (1 - topScalar), y1], //TR + [x1 + w1, y1 + h1 * midScalar], //MR + [x1 + w1 * (1 - botScalar), y1 + h1], //BR + [x1 + w1 * botScalar, y1 + h1], //BL + [x1, y1 + h1 * midScalar]) //ML + }); + svg.hydrate(el, {rx: 0.5, ry: 0.5, class: "sim-bb-wire-end"}); + (el).style["stroke-width"] = `${strokeWidth}px`; + + let el2 = svg.elt("rect"); + let h2 = metalWidth; + let w2 = metalHeight; + let cy2 = cy + o * (h1 / 2 + h2 / 2); + let x2 = cx - w2 / 2; + let y2 = cy2 - (h2 / 2); + svg.hydrate(el2, {x: x2, y: y2, width: w2, height: h2, class: "sim-bb-wire-bare-end"}); + + g.appendChild(el2); + g.appendChild(el); + return {el: g, x: x1 - strokeWidth, y: Math.min(y1, y2), w: w1 + strokeWidth * 2, h: h1 + h2}; + } + + //TODO: make this stupid class obsolete + export class WireFactory { + private underboard: SVGGElement; + private overboard: SVGGElement; + private boardEdges: number[]; + private getLocCoord: (loc: Loc) => Coord; + public styleEl: SVGStyleElement; + + constructor(underboard: SVGGElement, overboard: SVGGElement, boardEdges: number[], styleEl: SVGStyleElement, getLocCoord: (loc: Loc) => Coord) { + this.styleEl = styleEl; + this.styleEl.textContent += WIRES_CSS; + this.underboard = underboard; + this.overboard = overboard; + this.boardEdges = boardEdges; + this.getLocCoord = getLocCoord; + } + + private indexOfMin(vs: number[]): number { + let minIdx = 0; + let min = vs[0]; + for (let i = 1; i < vs.length; i++) { + if (vs[i] < min) { + min = vs[i]; + minIdx = i; + } + } + return minIdx; + } + private closestEdgeIdx(p: [number, number]): number { + let dists = this.boardEdges.map(e => Math.abs(p[1] - e)); + let edgeIdx = this.indexOfMin(dists); + return edgeIdx; + } + private closestEdge(p: [number, number]): number { + return this.boardEdges[this.closestEdgeIdx(p)]; + } + + private nextWireId = 0; + private drawWire(pin1: Coord, pin2: Coord, color: string): Wire { + let wires: SVGElement[] = []; + let g = svg.child(this.overboard, "g", {class: "sim-bb-wire-group"}); + const closestPointOffBoard = (p: [number, number]): [number, number] => { + const offset = PIN_DIST / 2; + let e = this.closestEdge(p); + let y: number; + if (e - p[1] < 0) + y = e - offset; + else + y = e + offset; + return [p[0], y]; + } + let wireId = this.nextWireId++; + let clrClass = cssEncodeColor(color); + let end1 = mkBBJumperEnd(pin1, clrClass); + let end2 = mkBBJumperEnd(pin2, clrClass); + let endG = svg.child(g, "g", {class: "sim-bb-wire-ends-g"}); + endG.appendChild(end1); + endG.appendChild(end2); + let edgeIdx1 = this.closestEdgeIdx(pin1); + let edgeIdx2 = this.closestEdgeIdx(pin2); + if (edgeIdx1 == edgeIdx2) { + let seg = mkWireSeg(pin1, pin2, clrClass); + g.appendChild(seg); + wires.push(seg); + } else { + let offP1 = closestPointOffBoard(pin1); + let offP2 = closestPointOffBoard(pin2); + let offSeg1 = mkWireSeg(pin1, offP1, clrClass); + let offSeg2 = mkWireSeg(pin2, offP2, clrClass); + let midSeg: SVGElement; + let midSegHover: SVGElement; + let isBetweenMiddleTwoEdges = (edgeIdx1 == 1 || edgeIdx1 == 2) && (edgeIdx2 == 1 || edgeIdx2 == 2); + if (isBetweenMiddleTwoEdges) { + midSeg = mkCurvedWireSeg(offP1, offP2, BB_WIRE_SMOOTH, clrClass); + midSegHover = mkCurvedWireSeg(offP1, offP2, BB_WIRE_SMOOTH, clrClass); + } else { + midSeg = mkWireSeg(offP1, offP2, clrClass); + midSegHover = mkWireSeg(offP1, offP2, clrClass); + } + svg.addClass(midSegHover, "sim-bb-wire-hover"); + g.appendChild(offSeg1); + wires.push(offSeg1); + g.appendChild(offSeg2); + wires.push(offSeg2); + this.underboard.appendChild(midSeg); + wires.push(midSeg); + g.appendChild(midSegHover); + wires.push(midSegHover); + //set hover mechanism + let wireIdClass = `sim-bb-wire-id-${wireId}`; + const setId = (e: SVGElement) => svg.addClass(e, wireIdClass); + setId(endG); + setId(midSegHover); + this.styleEl.textContent += ` + .${wireIdClass}:hover ~ .${wireIdClass}.sim-bb-wire-hover { + visibility: visible; + }` + } + + // wire colors + let colorCSS = ` + .wire-stroke-${clrClass} { + stroke: ${mapWireColor(color)}; + } + .wire-fill-${clrClass} { + fill: ${mapWireColor(color)}; + } + ` + this.styleEl.textContent += colorCSS; + + return {endG: endG, end1: end1, end2: end2, wires: wires}; + } + private drawWireWithCrocs(pin1: Coord, pin2: Coord, color: string): Wire { + //TODO: merge with drawWire() + const PIN_Y_OFF = 40; + const CROC_Y_OFF = -17; + let wires: SVGElement[] = []; + let g = svg.child(this.overboard, "g", {class: "sim-bb-wire-group"}); + const closestPointOffBoard = (p: [number, number]): [number, number] => { + const offset = PIN_DIST / 2; + let e = this.closestEdge(p); + let y: number; + if (e - p[1] < 0) + y = e - offset; + else + y = e + offset; + return [p[0], y]; + } + let wireId = this.nextWireId++; + let clrClass = cssEncodeColor(color); + let end1 = mkBBJumperEnd(pin1, clrClass); + let pin2orig = pin2; + let [x2, y2] = pin2; + pin2 = [x2, y2 + PIN_Y_OFF];//HACK + [x2, y2] = pin2; + let endCoord2: Coord = [x2, y2 + CROC_Y_OFF] + let end2AndSize = mkCrocEnd(endCoord2, true, color); + let end2 = end2AndSize.el; + let endG = svg.child(g, "g", {class: "sim-bb-wire-ends-g"}); + endG.appendChild(end1); + //endG.appendChild(end2); + let edgeIdx1 = this.closestEdgeIdx(pin1); + let edgeIdx2 = this.closestEdgeIdx(pin2orig); + if (edgeIdx1 == edgeIdx2) { + let seg = mkWireSeg(pin1, pin2, clrClass); + g.appendChild(seg); + wires.push(seg); + } else { + let offP1 = closestPointOffBoard(pin1); + //let offP2 = closestPointOffBoard(pin2orig); + let offSeg1 = mkWireSeg(pin1, offP1, clrClass); + //let offSeg2 = mkWireSeg(pin2, offP2, clrClass); + let midSeg: SVGElement; + let midSegHover: SVGElement; + let isBetweenMiddleTwoEdges = (edgeIdx1 == 1 || edgeIdx1 == 2) && (edgeIdx2 == 1 || edgeIdx2 == 2); + if (isBetweenMiddleTwoEdges) { + midSeg = mkCurvedWireSeg(offP1, pin2, BB_WIRE_SMOOTH, clrClass); + midSegHover = mkCurvedWireSeg(offP1, pin2, BB_WIRE_SMOOTH, clrClass); + } else { + midSeg = mkWireSeg(offP1, pin2, clrClass); + midSegHover = mkWireSeg(offP1, pin2, clrClass); + } + svg.addClass(midSegHover, "sim-bb-wire-hover"); + g.appendChild(offSeg1); + wires.push(offSeg1); + // g.appendChild(offSeg2); + // wires.push(offSeg2); + this.underboard.appendChild(midSeg); + wires.push(midSeg); + //g.appendChild(midSegHover); + //wires.push(midSegHover); + //set hover mechanism + let wireIdClass = `sim-bb-wire-id-${wireId}`; + const setId = (e: SVGElement) => svg.addClass(e, wireIdClass); + setId(endG); + setId(midSegHover); + this.styleEl.textContent += ` + .${wireIdClass}:hover ~ .${wireIdClass}.sim-bb-wire-hover { + visibility: visible; + }` + } + endG.appendChild(end2);//HACK + + // wire colors + let colorCSS = ` + .wire-stroke-${clrClass} { + stroke: ${mapWireColor(color)}; + } + .wire-fill-${clrClass} { + fill: ${mapWireColor(color)}; + } + ` + this.styleEl.textContent += colorCSS; + + return {endG: endG, end1: end1, end2: end2, wires: wires}; + } + + public addWire(start: Loc, end: Loc, color: string, withCrocs: boolean = false): Wire { + let startLoc = this.getLocCoord(start); + let endLoc = this.getLocCoord(end); + let wireEls: Wire; + if (withCrocs && end.type == "dalboard") { + wireEls = this.drawWireWithCrocs(startLoc, endLoc, color); + } else { + wireEls = this.drawWire(startLoc, endLoc, color); + } + return wireEls; + } + } +}