diff --git a/docs/static/hardware/.gitignore b/docs/static/hardware/.gitignore
new file mode 100644
index 00000000..3f724c25
--- /dev/null
+++ b/docs/static/hardware/.gitignore
@@ -0,0 +1,4 @@
+# don't check in until OSS request is approved
+neopixel-black-60-vert.svg
+sparkfun-*
+raspberrypi-*
\ 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 6bccf69d..75709fa5 100644
--- a/libs/microbit/music.ts
+++ b/libs/microbit/music.ts
@@ -135,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);
@@ -146,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);
@@ -157,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);
}
@@ -168,6 +171,7 @@ 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;
@@ -182,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;
@@ -198,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;
@@ -209,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);
}
@@ -220,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/pxtarget.json b/pxtarget.json
index c14da7b5..2b5c0f04 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..8baae2eb
--- /dev/null
+++ b/sim/dalboard.ts
@@ -0,0 +1,100 @@
+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;
+
+ //TODO: read from pxt.json/pxttarget.json
+ let boardDef = MICROBIT_DEF;
+ // let boardDef = ARDUINO_ZERO;
+ // let boardDef = SPARKFUN_PHOTON;
+ // let boardDef = RASPBERRYPI_MODELB;
+
+ 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..ca29a2b9
--- /dev/null
+++ b/sim/definitions.ts
@@ -0,0 +1,205 @@
+///
+///
+///
+///
+
+namespace pxsim {
+ export interface PinBlockDefinition {
+ x: number,
+ y: number,
+ 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[]
+ }
+ 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,
+ left: number,
+ top: number,
+ pinDist: 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"],
+ }
+ 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,
+ left: -180,
+ top: -135,
+ pinDist: 70,
+ },
+ breadboardColumnsNeeded: 5,
+ breadboardStartRow: "f",
+ pinAllocation: {
+ type: "auto",
+ gpioPinsNeeded: 1,
+ },
+ assemblyStep: 0,
+ wires: [
+ {start: ["breadboard", "j", 1], end: ["GPIO", 0], color: "white", assemblyStep: 1},
+ {start: ["breadboard", "j", 3], end: "ground", color: "white", 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),
+ };
+}
\ No newline at end of file
diff --git a/sim/instructions/instructions.ts b/sim/instructions/instructions.ts
new file mode 100644
index 00000000..9ae5a649
--- /dev/null
+++ b/sim/instructions/instructions.ts
@@ -0,0 +1,669 @@
+///
+///
+///
+///
+///
+///
+
+//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: BoardImageDefinition): visuals.SVGElAndSize {
+ return new visuals.MicrobitBoardSvg({
+ theme: visuals.randomTheme()
+ }).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(type: "wire" | string, opts: mkCmpDivOpts): HTMLElement {
+ let el: visuals.SVGElAndSize;
+ if (type == "wire") {
+ //TODO: support non-croc wire parts
+ el = visuals.mkWirePart([0, 0], opts.wireClr || "red", true);
+ } else {
+ let cnstr = builtinComponentPartVisual[type];
+ el = cnstr([0, 0]);
+ }
+ 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 rowCol: BBRowCol = [`${cmpInst.breadboardStartRow}`, `${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;
+ }
+ if (typeof c.visual === "string") {
+ let builtinVisual = c.visual;
+ let cmp = mkCmpDiv(builtinVisual, {
+ left: QUANT_LBL(quant),
+ leftSize: QUANT_LBL_SIZE,
+ cmpScale: PARTS_CMP_SCALE,
+ });
+ addClass(cmp, "partslist-cmp");
+ panel.appendChild(cmp);
+ } else {
+ //TODO: handle generic components
+ }
+ });
+
+ // 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;
+ if (typeof c.visual === "string") {
+ let builtinVisual = c.visual;
+ let cmp = mkCmpDiv(builtinVisual, {
+ top: `(${row},${col})`,
+ topSize: LOC_LBL_SIZE,
+ cmpHeight: REQ_CMP_HEIGHT,
+ cmpScale: REQ_CMP_SCALE
+ })
+ addClass(cmp, "cmp-div");
+ reqsDiv.appendChild(cmp);
+ } else {
+ //TODO: generic component
+ }
+ });
+ });
+
+ 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 = MICROBIT_DEF;
+ 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..334cc44f
--- /dev/null
+++ b/sim/simlib.ts
@@ -0,0 +1,218 @@
+///
+///
+///
+
+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 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..9a44d849
--- /dev/null
+++ b/sim/visuals/boardhost.ts
@@ -0,0 +1,181 @@
+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;
+
+ 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.boardView = new visuals.MicrobitBoardSvg({
+ runtime: runtime,
+ theme: visuals.randomTheme(),
+ disableTilt: false,
+ wireframe: opts.wireframe,
+ });
+
+ let useBreadboard = 0 < activeComponents.length || opts.forceBreadboard;
+ if (useBreadboard) {
+ this.breadboard = new Breadboard({
+ wireframe: opts.wireframe,
+ });
+ let composition = composeSVG({
+ el1: this.boardView.getView(),
+ scaleUnit1: this.boardView.getPinDist(),
+ el2: this.breadboard.getSVGAndSize(),
+ scaleUnit2: this.breadboard.getPinDist(),
+ margin: [0, 0, 20, 0],
+ middleMargin: 80,
+ 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;
+ 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);
+ 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}`, `${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();
+ } else {
+ let vis = cmpDesc.visual as PartVisualDefinition;
+ console.log("TODO PART: " + vis.image);
+ //TODO: support generic parts
+ }
+ return cmp;
+ }
+ public addWire(inst: WireInst): Wire {
+ return this.wireFactory.addWire(inst.start, inst.end, inst.color, true);
+ }
+ 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