Merge pull request #223 from Microsoft/breadboarding
wires, breadboarding, hardware components and Arduino support
This commit is contained in:
commit
edc489c83d
4
docs/static/hardware/.gitignore
vendored
Normal file
4
docs/static/hardware/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# don't check in until OSS request is approved
|
||||||
|
neopixel-black-60-vert.svg
|
||||||
|
sparkfun-*
|
||||||
|
raspberrypi-*
|
32
docs/static/hardware/neopixel.svg
vendored
Normal file
32
docs/static/hardware/neopixel.svg
vendored
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg viewBox="-5 -1 53 112" xmlns="http://www.w3.org/2000/svg" xmlns:bx="https://boxy-svg.com">
|
||||||
|
<rect x="2.5" width="38" height="100" style="fill: rgb(68, 68, 68);"/>
|
||||||
|
<rect x="11.748" y="3.2" width="1.391" height="2.553" style="fill: none; stroke-linejoin: round; stroke-width: 3; stroke: rgb(165, 103, 52);"/>
|
||||||
|
<rect x="20.75" y="3.2" width="1.391" height="2.553" style="fill: none; stroke-linejoin: round; stroke-width: 3; stroke: rgb(165, 103, 52);"/>
|
||||||
|
<rect x="29.75" y="3.2" width="1.391" height="2.553" style="fill: none; stroke-linejoin: round; stroke-width: 3; stroke: rgb(165, 103, 52);"/>
|
||||||
|
<g>
|
||||||
|
<rect x="9" y="16.562" width="25" height="3.238" style="fill: rgb(216, 216, 216);"/>
|
||||||
|
<rect x="9" y="22.562" width="25" height="3.238" style="fill: rgb(216, 216, 216);"/>
|
||||||
|
<rect x="9" y="28.563" width="25" height="3.238" style="fill: rgb(216, 216, 216);"/>
|
||||||
|
<rect x="11.607" y="14.833" width="19.787" height="18.697" style="fill: rgb(0, 0, 0);"/>
|
||||||
|
<ellipse style="fill: rgb(216, 216, 216);" cx="21.5" cy="24.181" rx="7" ry="7"/>
|
||||||
|
</g>
|
||||||
|
<path d="M -7.25 -103.2 L -2.5 -100.003 L -12 -100.003 L -7.25 -103.2 Z" style="fill: rgb(68, 68, 68);" transform="matrix(-1, 0, 0, -1, 0, 0)" bx:shape="triangle -12 -103.2 9.5 3.197 0.5 0 1@ad6f5cac"/>
|
||||||
|
<path d="M -16.75 -103.197 L -12 -100 L -21.5 -100 L -16.75 -103.197 Z" style="fill: rgb(68, 68, 68);" transform="matrix(-1, 0, 0, -1, 0, 0)" bx:shape="triangle -21.5 -103.197 9.5 3.197 0.5 0 1@07d73149"/>
|
||||||
|
<path d="M -26.25 -103.2 L -21.5 -100.003 L -31 -100.003 L -26.25 -103.2 Z" style="fill: rgb(68, 68, 68);" transform="matrix(-1, 0, 0, -1, 0, 0)" bx:shape="triangle -31 -103.2 9.5 3.197 0.5 0 1@54403e2d"/>
|
||||||
|
<path d="M -35.75 -103.197 L -31 -100 L -40.5 -100 L -35.75 -103.197 Z" style="fill: rgb(68, 68, 68);" transform="matrix(-1, 0, 0, -1, 0, 0)" bx:shape="triangle -40.5 -103.197 9.5 3.197 0.5 0 1@21c9b772"/>
|
||||||
|
<g transform="matrix(1, 0, 0, 1, 0.000002, 29.999994)">
|
||||||
|
<rect x="9" y="16.562" width="25" height="3.238" style="fill: rgb(216, 216, 216);"/>
|
||||||
|
<rect x="9" y="22.562" width="25" height="3.238" style="fill: rgb(216, 216, 216);"/>
|
||||||
|
<rect x="9" y="28.563" width="25" height="3.238" style="fill: rgb(216, 216, 216);"/>
|
||||||
|
<rect x="11.607" y="14.833" width="19.787" height="18.697" style="fill: rgb(0, 0, 0);"/>
|
||||||
|
<ellipse style="fill: rgb(216, 216, 216);" cx="21.5" cy="24.181" rx="7" ry="7"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(1, 0, 0, 1, 0.000005, 59.999992)">
|
||||||
|
<rect x="9" y="16.562" width="25" height="3.238" style="fill: rgb(216, 216, 216);"/>
|
||||||
|
<rect x="9" y="22.562" width="25" height="3.238" style="fill: rgb(216, 216, 216);"/>
|
||||||
|
<rect x="9" y="28.563" width="25" height="3.238" style="fill: rgb(216, 216, 216);"/>
|
||||||
|
<rect x="11.607" y="14.833" width="19.787" height="18.697" style="fill: rgb(0, 0, 0);"/>
|
||||||
|
<ellipse style="fill: rgb(216, 216, 216);" cx="21.5" cy="24.181" rx="7" ry="7"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.9 KiB |
35
docs/static/hardware/speaker.svg
vendored
Normal file
35
docs/static/hardware/speaker.svg
vendored
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g transform="matrix(1, 0, 0, 1, -0.00023, -58.230297)">
|
||||||
|
<ellipse style="fill: rgb(70, 70, 70);" cx="250.58" cy="308.81" rx="215" ry="215"/>
|
||||||
|
<ellipse style="fill: rgb(0, 0, 0);" transform="matrix(1, 0, 0, 1.000001, -232.069031, 248.780606)" cx="482.069" cy="198.188" rx="23.028" ry="23.028"/>
|
||||||
|
<ellipse style="fill: rgb(0, 0, 0);" transform="matrix(1, 0, 0, 0.999999, -232.067871, 110.041956)" cx="482.067" cy="198.188" rx="23.028" ry="23.028"/>
|
||||||
|
<ellipse style="fill: rgb(0, 0, 0);" cx="389.12" cy="308.23" rx="23.028" ry="23.028"/>
|
||||||
|
<ellipse style="fill: rgb(0, 0, 0);" cx="110.88" cy="308.23" rx="23.028" ry="23.028"/>
|
||||||
|
<ellipse style="fill: rgb(0, 0, 0);" cx="250" cy="169.393" rx="23.028" ry="23.028"/>
|
||||||
|
<g transform="matrix(1, 0, 0, 1, -0.000009, 0.000015)">
|
||||||
|
<ellipse style="fill: rgb(0, 0, 0);" cx="250" cy="238.513" rx="23.028" ry="23.028" transform="matrix(1.000001, 0, 0, 0.999999, 69.996739, 69.71816)"/>
|
||||||
|
<ellipse style="fill: rgb(0, 0, 0);" transform="matrix(1, 0, 0, 0.999999, -302.064453, 110.043115)" cx="482.064" cy="198.188" rx="23.028" ry="23.028"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.866026, 0.5, -0.5, 0.866026, 7.386552, -105.261086)">
|
||||||
|
<ellipse style="fill: rgb(0, 0, 0);" transform="matrix(0.999999, 0, 0, 0.999999, -65.212313, 177.387415)" cx="482.068" cy="198.188" rx="23.028" ry="23.028"/>
|
||||||
|
<ellipse style="fill: rgb(0, 0, 0);" cx="555.975" cy="236.836" rx="23.028" ry="23.028"/>
|
||||||
|
<ellipse style="fill: rgb(0, 0, 0);" cx="277.735" cy="236.836" rx="23.028" ry="23.028"/>
|
||||||
|
<ellipse style="fill: rgb(0, 0, 0);" cx="416.855" cy="97.999" rx="23.028" ry="23.028"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(0.5, 0.866026, -0.866026, 0.5, 246.635941, -171.170502)">
|
||||||
|
<ellipse style="fill: rgb(0, 0, 0);" transform="matrix(0.999999, 0, 0, 0.999999, -65.212313, 177.387415)" cx="482.068" cy="198.188" rx="23.028" ry="23.028"/>
|
||||||
|
<ellipse style="fill: rgb(0, 0, 0);" cx="555.975" cy="236.836" rx="23.028" ry="23.028"/>
|
||||||
|
<ellipse style="fill: rgb(0, 0, 0);" cx="277.735" cy="236.836" rx="23.028" ry="23.028"/>
|
||||||
|
<ellipse style="fill: rgb(0, 0, 0);" cx="416.855" cy="97.999" rx="23.028" ry="23.028"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(-0.5, 0.866026, -0.866026, -0.5, 641.934998, 245.84082)">
|
||||||
|
<ellipse style="fill: rgb(0, 0, 0);" cx="250" cy="238.513" rx="23.028" ry="23.028" transform="matrix(1.000001, 0, 0, 0.999999, 69.996739, 69.71816)"/>
|
||||||
|
<ellipse style="fill: rgb(0, 0, 0);" transform="matrix(1, 0, 0, 0.999999, -302.064453, 110.043115)" cx="482.064" cy="198.188" rx="23.028" ry="23.028"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(-0.500001, -0.866026, 0.866026, -0.500001, 108.063393, 678.85083)">
|
||||||
|
<ellipse style="fill: rgb(0, 0, 0);" cx="250" cy="238.513" rx="23.028" ry="23.028" transform="matrix(1.000001, 0, 0, 0.999999, 69.996739, 69.71816)"/>
|
||||||
|
<ellipse style="fill: rgb(0, 0, 0);" transform="matrix(1, 0, 0, 0.999999, -302.064453, 110.043115)" cx="482.064" cy="198.188" rx="23.028" ry="23.028"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.1 KiB |
|
@ -135,6 +135,7 @@ namespace music {
|
||||||
*/
|
*/
|
||||||
//% help=music/play-tone weight=90
|
//% help=music/play-tone weight=90
|
||||||
//% blockId=device_play_note block="play|tone %note=device_note|for %duration=device_beat" icon="\uf025" blockGap=8
|
//% 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 {
|
export function playTone(frequency: number, ms: number): void {
|
||||||
pins.analogSetPitchPin(AnalogPin.P0);
|
pins.analogSetPitchPin(AnalogPin.P0);
|
||||||
pins.analogPitch(frequency, ms);
|
pins.analogPitch(frequency, ms);
|
||||||
|
@ -146,6 +147,7 @@ namespace music {
|
||||||
*/
|
*/
|
||||||
//% help=music/ring-tone weight=80
|
//% help=music/ring-tone weight=80
|
||||||
//% blockId=device_ring block="ring tone (Hz)|%note=device_note" icon="\uf025" blockGap=8
|
//% blockId=device_ring block="ring tone (Hz)|%note=device_note" icon="\uf025" blockGap=8
|
||||||
|
//% parts="speaker"
|
||||||
export function ringTone(frequency: number): void {
|
export function ringTone(frequency: number): void {
|
||||||
pins.analogSetPitchPin(AnalogPin.P0);
|
pins.analogSetPitchPin(AnalogPin.P0);
|
||||||
pins.analogPitch(frequency, 0);
|
pins.analogPitch(frequency, 0);
|
||||||
|
@ -157,6 +159,7 @@ namespace music {
|
||||||
*/
|
*/
|
||||||
//% help=music/rest weight=79
|
//% help=music/rest weight=79
|
||||||
//% blockId=device_rest block="rest(ms)|%duration=device_beat"
|
//% blockId=device_rest block="rest(ms)|%duration=device_beat"
|
||||||
|
//% parts="speaker"
|
||||||
export function rest(ms: number): void {
|
export function rest(ms: number): void {
|
||||||
playTone(0, ms);
|
playTone(0, ms);
|
||||||
}
|
}
|
||||||
|
@ -168,6 +171,7 @@ namespace music {
|
||||||
*/
|
*/
|
||||||
//% weight=50 help=music/note-frequency
|
//% weight=50 help=music/note-frequency
|
||||||
//% blockId=device_note block="%note"
|
//% blockId=device_note block="%note"
|
||||||
|
//% parts="speaker"
|
||||||
//% shim=TD_ID
|
//% shim=TD_ID
|
||||||
export function noteFrequency(name: Note): number {
|
export function noteFrequency(name: Note): number {
|
||||||
return name;
|
return name;
|
||||||
|
@ -182,6 +186,7 @@ namespace music {
|
||||||
*/
|
*/
|
||||||
//% help=music/beat weight=49
|
//% help=music/beat weight=49
|
||||||
//% blockId=device_beat block="%fraction|beat"
|
//% blockId=device_beat block="%fraction|beat"
|
||||||
|
//% parts="speaker"
|
||||||
export function beat(fraction?: BeatFraction): number {
|
export function beat(fraction?: BeatFraction): number {
|
||||||
init();
|
init();
|
||||||
if (fraction == null) fraction = BeatFraction.Whole;
|
if (fraction == null) fraction = BeatFraction.Whole;
|
||||||
|
@ -198,6 +203,7 @@ namespace music {
|
||||||
*/
|
*/
|
||||||
//% help=music/tempo weight=40
|
//% help=music/tempo weight=40
|
||||||
//% blockId=device_tempo block="tempo (bpm)" blockGap=8
|
//% blockId=device_tempo block="tempo (bpm)" blockGap=8
|
||||||
|
//% parts="speaker"
|
||||||
export function tempo(): number {
|
export function tempo(): number {
|
||||||
init();
|
init();
|
||||||
return beatsPerMinute;
|
return beatsPerMinute;
|
||||||
|
@ -209,8 +215,9 @@ namespace music {
|
||||||
*/
|
*/
|
||||||
//% help=music/change-tempo weight=39
|
//% help=music/change-tempo weight=39
|
||||||
//% blockId=device_change_tempo block="change tempo by (bpm)|%value" blockGap=8
|
//% blockId=device_change_tempo block="change tempo by (bpm)|%value" blockGap=8
|
||||||
|
//% parts="speaker"
|
||||||
export function changeTempoBy(bpm: number): void {
|
export function changeTempoBy(bpm: number): void {
|
||||||
init();
|
init();
|
||||||
setTempo(beatsPerMinute + bpm);
|
setTempo(beatsPerMinute + bpm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -220,6 +227,7 @@ namespace music {
|
||||||
*/
|
*/
|
||||||
//% help=music/set-tempo weight=38
|
//% help=music/set-tempo weight=38
|
||||||
//% blockId=device_set_tempo block="set tempo to (bpm)|%value"
|
//% blockId=device_set_tempo block="set tempo to (bpm)|%value"
|
||||||
|
//% parts="speaker"
|
||||||
export function setTempo(bpm: number): void {
|
export function setTempo(bpm: number): void {
|
||||||
init();
|
init();
|
||||||
if (bpm > 0) {
|
if (bpm > 0) {
|
||||||
|
|
|
@ -75,7 +75,17 @@
|
||||||
},
|
},
|
||||||
"simulator": {
|
"simulator": {
|
||||||
"autoRun": true,
|
"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": {
|
"compileService": {
|
||||||
"yottaTarget": "bbc-microbit-classic-gcc",
|
"yottaTarget": "bbc-microbit-classic-gcc",
|
||||||
|
|
432
sim/allocator.ts
Normal file
432
sim/allocator.ts
Normal file
|
@ -0,0 +1,432 @@
|
||||||
|
|
||||||
|
namespace pxsim {
|
||||||
|
export interface AllocatorOpts {
|
||||||
|
boardDef: BoardDefinition,
|
||||||
|
cmpDefs: Map<PartDefinition>,
|
||||||
|
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 => <BBRowCol>["+", `${n}`]),
|
||||||
|
ground: mkRange(26, 51).map(n => <BBRowCol>["-", `${n}`]),
|
||||||
|
},
|
||||||
|
bottom: {
|
||||||
|
threeVolt: mkRange(1, 26).map(n => <BBRowCol>["+", `${n}`]),
|
||||||
|
ground: mkRange(1, 26).map(n => <BBRowCol>["-", `${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 = <string>location[1];
|
||||||
|
let col = (<number>location[2] + opts.startColumn).toString();
|
||||||
|
return {type: "breadboard", rowCol: [row, col]}
|
||||||
|
} else if (location[0] === "GPIO") {
|
||||||
|
U.assert(!!opts.cmpGPIOPins);
|
||||||
|
let idx = <number>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 = <BBLoc>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 = (<PredefinedPinAlloc>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 = (<FactoryFunctionPinAlloc>def.pinAllocation);
|
||||||
|
let fnNm = fnPinAlloc.functionName;
|
||||||
|
let fnsAndArgs = <string[]>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 = (<AutoPinAlloc>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 = <number[]>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();
|
||||||
|
}
|
||||||
|
}
|
100
sim/dalboard.ts
Normal file
100
sim/dalboard.ts
Normal file
|
@ -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 = <SimulatorEventBusMessage>msg;
|
||||||
|
this.bus.queue(ev.id, ev.eventid, ev.value);
|
||||||
|
break;
|
||||||
|
case "serial":
|
||||||
|
let data = (<SimulatorSerialMessage>msg).data || "";
|
||||||
|
this.serialState.recieveData(data);
|
||||||
|
break;
|
||||||
|
case "radiopacket":
|
||||||
|
let packet = <SimulatorRadioPacketMessage>msg;
|
||||||
|
this.radioState.recievePacket(packet);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kill() {
|
||||||
|
super.kill();
|
||||||
|
AudioContextManager.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
initAsync(msg: SimulatorRunMessage): Promise<void> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
205
sim/definitions.ts
Normal file
205
sim/definitions.ts
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
/// <reference path="../node_modules/pxt-core/typings/bluebird/bluebird.d.ts"/>
|
||||||
|
/// <reference path="../node_modules/pxt-core/built/pxtsim.d.ts"/>
|
||||||
|
/// <reference path="../libs/microbit/dal.d.ts"/>
|
||||||
|
/// <reference path="./visuals/neopixel.ts"/>
|
||||||
|
|
||||||
|
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<PartDefinition> = {
|
||||||
|
"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<any>> = {
|
||||||
|
"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),
|
||||||
|
};
|
||||||
|
}
|
669
sim/instructions/instructions.ts
Normal file
669
sim/instructions/instructions.ts
Normal file
|
@ -0,0 +1,669 @@
|
||||||
|
/// <reference path="../../node_modules/pxt-core/typings/bluebird/bluebird.d.ts"/>
|
||||||
|
/// <reference path="../../node_modules/pxt-core/built/pxtsim.d.ts"/>
|
||||||
|
/// <reference path="../../node_modules/pxt-core/built/pxtrunner.d.ts"/>
|
||||||
|
/// <reference path="../../libs/microbit/dal.d.ts"/>
|
||||||
|
/// <reference path="../visuals/genericboard.ts"/>
|
||||||
|
/// <reference path="../visuals/wiring.ts"/>
|
||||||
|
|
||||||
|
//HACK: allows instructions.html to access pxtblocks without requiring simulator.html to import blocks as well
|
||||||
|
if (!(<any>window).pxt) (<any>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 = <SVGSVGElement>document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||||
|
let dims = {l: 0, t: 0, w: 0, h: 0};
|
||||||
|
|
||||||
|
let cmpSvgEl = <SVGSVGElement>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;
|
||||||
|
(<any>cmpSvgAtts).width = dims.w;
|
||||||
|
(<any>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<PartDefinition>,
|
||||||
|
fnArgs: any,
|
||||||
|
allAlloc: AllocatorResult,
|
||||||
|
stepToWires: WireInst[][],
|
||||||
|
stepToCmps: CmpInst[][]
|
||||||
|
allWires: WireInst[],
|
||||||
|
allCmps: CmpInst[],
|
||||||
|
lastStep: number,
|
||||||
|
colorToWires: Map<WireInst[]>,
|
||||||
|
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<WireInst[]> = {}
|
||||||
|
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<PartDefinition>, 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((<BBLoc>w.start).rowCol);
|
||||||
|
} else {
|
||||||
|
board.highlightBoardPin((<BoardLoc>w.start).pin);
|
||||||
|
}
|
||||||
|
if (w.end.type == "breadboard") {
|
||||||
|
let [row, col] = (<BBLoc>w.end).rowCol;
|
||||||
|
let lbls = board.highlightBreadboardPin((<BBLoc>w.end).rowCol);
|
||||||
|
} else {
|
||||||
|
board.highlightBoardPin((<BoardLoc>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(<BoardImageDefinition>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 = <string>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] = (<BBLoc>loc).rowCol;
|
||||||
|
return `(${row},${col})`
|
||||||
|
} else
|
||||||
|
return (<BoardLoc>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 = <string>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 (<any>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);
|
||||||
|
}
|
||||||
|
}
|
763
sim/libmbit.ts
763
sim/libmbit.ts
|
@ -1,763 +0,0 @@
|
||||||
/// <reference path="../node_modules/pxt-core/typings/bluebird/bluebird.d.ts"/>
|
|
||||||
/// <reference path="../node_modules/pxt-core/built/pxtsim.d.ts"/>
|
|
||||||
/// <reference path="../libs/microbit/dal.d.ts"/>
|
|
||||||
|
|
||||||
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<boolean>((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 {
|
|
||||||
(<any>window).AudioContext = (<any>window).AudioContext || (<any>window).webkitAudioContext;
|
|
||||||
if ((<any>window).AudioContext) {
|
|
||||||
try {
|
|
||||||
// this call my crash.
|
|
||||||
// SyntaxError: audio resources unavailable for AudioContext construction
|
|
||||||
return new (<any>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 <string>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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,183 @@
|
||||||
namespace pxsim.micro_bit {
|
namespace pxsim.visuals {
|
||||||
const svg = pxsim.svg;
|
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 {
|
export interface IBoardTheme {
|
||||||
accent?: string;
|
accent?: string;
|
||||||
display?: string;
|
display?: string;
|
||||||
|
@ -43,9 +220,10 @@ namespace pxsim.micro_bit {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBoardProps {
|
export interface IBoardProps {
|
||||||
runtime: pxsim.Runtime;
|
runtime?: pxsim.Runtime;
|
||||||
theme?: IBoardTheme;
|
theme?: IBoardTheme;
|
||||||
disableTilt?: boolean;
|
disableTilt?: boolean;
|
||||||
|
wireframe?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pointerEvents = !!(window as any).PointerEvent ? {
|
const pointerEvents = !!(window as any).PointerEvent ? {
|
||||||
|
@ -60,11 +238,11 @@ namespace pxsim.micro_bit {
|
||||||
leave: "mouseleave"
|
leave: "mouseleave"
|
||||||
};
|
};
|
||||||
|
|
||||||
export class MicrobitBoardSvg {
|
export class MicrobitBoardSvg implements BoardView {
|
||||||
public element: SVGSVGElement;
|
public element: SVGSVGElement;
|
||||||
private style: SVGStyleElement;
|
private style: SVGStyleElement;
|
||||||
private defs: SVGDefsElement;
|
private defs: SVGDefsElement;
|
||||||
private g: SVGElement;
|
private g: SVGGElement;
|
||||||
|
|
||||||
private logos: SVGElement[];
|
private logos: SVGElement[];
|
||||||
private head: SVGGElement; private headInitialized = false;
|
private head: SVGGElement; private headInitialized = false;
|
||||||
|
@ -88,15 +266,54 @@ namespace pxsim.micro_bit {
|
||||||
private thermometerText: SVGTextElement;
|
private thermometerText: SVGTextElement;
|
||||||
private shakeButton: SVGCircleElement;
|
private shakeButton: SVGCircleElement;
|
||||||
private shakeText: SVGTextElement;
|
private shakeText: SVGTextElement;
|
||||||
public board: pxsim.Board;
|
public board: pxsim.DalBoard;
|
||||||
|
private pinNmToCoord: Map<Coord> = {};
|
||||||
|
|
||||||
constructor(public props: IBoardProps) {
|
constructor(public props: IBoardProps) {
|
||||||
this.board = this.props.runtime.board as pxsim.Board;
|
this.recordPinCoords();
|
||||||
this.board.updateView = () => this.updateState();
|
|
||||||
this.buildDom();
|
this.buildDom();
|
||||||
this.updateTheme();
|
if (props && props.wireframe)
|
||||||
this.updateState();
|
svg.addClass(this.element, "sim-wireframe");
|
||||||
this.attachEvents();
|
|
||||||
|
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<SVGSVGElement> {
|
||||||
|
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() {
|
private updateTheme() {
|
||||||
|
@ -123,12 +340,14 @@ namespace pxsim.micro_bit {
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
let theme = this.props.theme;
|
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);
|
svg.fill(this.buttons[index], btn.pressed ? theme.buttonDown : theme.buttonUp);
|
||||||
});
|
});
|
||||||
|
|
||||||
let bw = state.displayMode == pxsim.DisplayMode.bw
|
let bw = state.ledMatrixState.displayMode == pxsim.DisplayMode.bw
|
||||||
let img = state.image;
|
let img = state.ledMatrixState.image;
|
||||||
this.leds.forEach((led, i) => {
|
this.leds.forEach((led, i) => {
|
||||||
let sel = (<SVGStylable><any>led)
|
let sel = (<SVGStylable><any>led)
|
||||||
sel.style.opacity = ((bw ? img.data[i] > 0 ? 255 : 0 : img.data[i]) / 255.0) + "";
|
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() {
|
private updateGestures() {
|
||||||
let state = this.board;
|
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;
|
this.shakeButton = svg.child(this.g, "circle", { cx: 380, cy: 100, r: 16.5 }) as SVGCircleElement;
|
||||||
svg.fill(this.shakeButton, this.props.theme.virtualButtonUp)
|
svg.fill(this.shakeButton, this.props.theme.virtualButtonUp)
|
||||||
this.shakeButton.addEventListener(pointerEvents.down, ev => {
|
this.shakeButton.addEventListener(pointerEvents.down, ev => {
|
||||||
|
@ -170,7 +389,7 @@ namespace pxsim.micro_bit {
|
||||||
|
|
||||||
private updateButtonAB() {
|
private updateButtonAB() {
|
||||||
let state = this.board;
|
let state = this.board;
|
||||||
if (state.usesButtonAB && !this.buttonABText) {
|
if (state.buttonPairState.usesButtonAB && !this.buttonABText) {
|
||||||
(<any>this.buttonsOuter[2]).style.visibility = "visible";
|
(<any>this.buttonsOuter[2]).style.visibility = "visible";
|
||||||
(<any>this.buttons[2]).style.visibility = "visible";
|
(<any>this.buttons[2]).style.visibility = "visible";
|
||||||
this.buttonABText = svg.child(this.g, "text", { class: "sim-text", x: 370, y: 272 }) as SVGTextElement;
|
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() {
|
private updateTemperature() {
|
||||||
let state = this.board;
|
let state = this.board;
|
||||||
if (!state || !state.usesTemperature) return;
|
if (!state || !state.thermometerState.usesTemperature) return;
|
||||||
|
|
||||||
let tmin = -5;
|
let tmin = -5;
|
||||||
let tmax = 50;
|
let tmax = 50;
|
||||||
|
@ -227,13 +446,13 @@ namespace pxsim.micro_bit {
|
||||||
(ev) => {
|
(ev) => {
|
||||||
let cur = svg.cursorPoint(pt, this.element, ev);
|
let cur = svg.cursorPoint(pt, this.element, ev);
|
||||||
let t = Math.max(0, Math.min(1, (260 - cur.y) / 140))
|
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();
|
this.updateTemperature();
|
||||||
}, ev => { }, ev => { })
|
}, ev => { }, ev => { })
|
||||||
}
|
}
|
||||||
|
|
||||||
let t = Math.max(tmin, Math.min(tmax, state.temperature))
|
let t = Math.max(tmin, Math.min(tmax, state.thermometerState.temperature))
|
||||||
let per = Math.floor((state.temperature - tmin) / (tmax - tmin) * 100)
|
let per = Math.floor((state.thermometerState.temperature - tmin) / (tmax - tmin) * 100)
|
||||||
svg.setGradientValue(this.thermometerGradient, 100 - per + "%");
|
svg.setGradientValue(this.thermometerGradient, 100 - per + "%");
|
||||||
this.thermometerText.textContent = t + "°C";
|
this.thermometerText.textContent = t + "°C";
|
||||||
}
|
}
|
||||||
|
@ -242,7 +461,7 @@ namespace pxsim.micro_bit {
|
||||||
let xc = 258;
|
let xc = 258;
|
||||||
let yc = 75;
|
let yc = 75;
|
||||||
let state = this.board;
|
let state = this.board;
|
||||||
if (!state || !state.usesHeading) return;
|
if (!state || !state.compassState.usesHeading) return;
|
||||||
if (!this.headInitialized) {
|
if (!this.headInitialized) {
|
||||||
let p = this.head.firstChild.nextSibling as SVGPathElement;
|
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");
|
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,
|
this.head,
|
||||||
(ev: MouseEvent) => {
|
(ev: MouseEvent) => {
|
||||||
let cur = svg.cursorPoint(pt, this.element, ev);
|
let cur = svg.cursorPoint(pt, this.element, ev);
|
||||||
state.heading = Math.floor(Math.atan2(cur.y - yc, cur.x - xc) * 180 / Math.PI + 90);
|
state.compassState.heading = Math.floor(Math.atan2(cur.y - yc, cur.x - xc) * 180 / Math.PI + 90);
|
||||||
if (state.heading < 0) state.heading += 360;
|
if (state.compassState.heading < 0) state.compassState.heading += 360;
|
||||||
this.updateHeading();
|
this.updateHeading();
|
||||||
});
|
});
|
||||||
this.headInitialized = true;
|
this.headInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let txt = state.heading.toString() + "°";
|
let txt = state.compassState.heading.toString() + "°";
|
||||||
if (txt != this.headText.textContent) {
|
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;
|
this.headText.textContent = txt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -297,12 +516,12 @@ namespace pxsim.micro_bit {
|
||||||
let state = this.board;
|
let state = this.board;
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
|
|
||||||
state.pins.forEach((pin, i) => this.updatePin(pin, i));
|
state.edgeConnectorState.pins.forEach((pin, i) => this.updatePin(pin, i));
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateLightLevel() {
|
private updateLightLevel() {
|
||||||
let state = this.board;
|
let state = this.board;
|
||||||
if (!state || !state.usesLightLevel) return;
|
if (!state || !state.lightSensorState.usesLightLevel) return;
|
||||||
|
|
||||||
if (!this.lightLevelButton) {
|
if (!this.lightLevelButton) {
|
||||||
let gid = "gradient-light-level";
|
let gid = "gradient-light-level";
|
||||||
|
@ -320,8 +539,8 @@ namespace pxsim.micro_bit {
|
||||||
let pos = svg.cursorPoint(pt, this.element, ev);
|
let pos = svg.cursorPoint(pt, this.element, ev);
|
||||||
let rs = r / 2;
|
let rs = r / 2;
|
||||||
let level = Math.max(0, Math.min(255, Math.floor((pos.y - (cy - rs)) / (2 * rs) * 255)));
|
let level = Math.max(0, Math.min(255, Math.floor((pos.y - (cy - rs)) / (2 * rs) * 255)));
|
||||||
if (level != this.board.lightLevel) {
|
if (level != this.board.lightSensorState.lightLevel) {
|
||||||
this.board.lightLevel = level;
|
this.board.lightSensorState.lightLevel = level;
|
||||||
this.applyLightLevel();
|
this.applyLightLevel();
|
||||||
}
|
}
|
||||||
}, ev => { },
|
}, ev => { },
|
||||||
|
@ -330,12 +549,12 @@ namespace pxsim.micro_bit {
|
||||||
this.updateTheme();
|
this.updateTheme();
|
||||||
}
|
}
|
||||||
|
|
||||||
svg.setGradientValue(this.lightLevelGradient, Math.min(100, Math.max(0, Math.floor(state.lightLevel * 100 / 255))) + '%')
|
svg.setGradientValue(this.lightLevelGradient, Math.min(100, Math.max(0, Math.floor(state.lightSensorState.lightLevel * 100 / 255))) + '%')
|
||||||
this.lightLevelText.textContent = state.lightLevel.toString();
|
this.lightLevelText.textContent = state.lightSensorState.lightLevel.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyLightLevel() {
|
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))) + '%')
|
svg.setGradientValue(this.lightLevelGradient, Math.min(100, Math.max(0, Math.floor(lv * 100 / 255))) + '%')
|
||||||
this.lightLevelText.textContent = lv.toString();
|
this.lightLevelText.textContent = lv.toString();
|
||||||
}
|
}
|
||||||
|
@ -343,10 +562,10 @@ namespace pxsim.micro_bit {
|
||||||
private updateTilt() {
|
private updateTilt() {
|
||||||
if (this.props.disableTilt) return;
|
if (this.props.disableTilt) return;
|
||||||
let state = this.board;
|
let state = this.board;
|
||||||
if (!state || !state.accelerometer.isActive) return;
|
if (!state || !state.accelerometerState.accelerometer.isActive) return;
|
||||||
|
|
||||||
let x = state.accelerometer.getX();
|
let x = state.accelerometerState.accelerometer.getX();
|
||||||
let y = state.accelerometer.getY();
|
let y = state.accelerometerState.accelerometer.getY();
|
||||||
let af = 8 / 1023;
|
let af = 8 / 1023;
|
||||||
|
|
||||||
this.element.style.transform = "perspective(30em) rotateX(" + y * af + "deg) rotateY(" + x * af + "deg)"
|
this.element.style.transform = "perspective(30em) rotateX(" + y * af + "deg) rotateY(" + x * af + "deg)"
|
||||||
|
@ -358,129 +577,18 @@ namespace pxsim.micro_bit {
|
||||||
this.element = <SVGSVGElement>svg.elt("svg")
|
this.element = <SVGSVGElement>svg.elt("svg")
|
||||||
svg.hydrate(this.element, {
|
svg.hydrate(this.element, {
|
||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
"viewBox": "0 0 498 406",
|
"viewBox": `0 0 ${MB_WIDTH} ${MB_HEIGHT}`,
|
||||||
"enable-background": "new 0 0 498 406",
|
|
||||||
"class": "sim",
|
"class": "sim",
|
||||||
"x": "0px",
|
"x": "0px",
|
||||||
"y": "0px"
|
"y": "0px",
|
||||||
|
"width": MB_WIDTH + "px",
|
||||||
|
"height": MB_HEIGHT + "px",
|
||||||
});
|
});
|
||||||
this.style = <SVGStyleElement>svg.child(this.element, "style", {});
|
this.style = <SVGStyleElement>svg.child(this.element, "style", {});
|
||||||
this.style.textContent = `
|
this.style.textContent = 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; }
|
|
||||||
}
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
|
|
||||||
this.defs = <SVGDefsElement>svg.child(this.element, "defs", {});
|
this.defs = <SVGDefsElement>svg.child(this.element, "defs", {});
|
||||||
this.g = svg.elt("g");
|
this.g = <SVGGElement>svg.elt("g");
|
||||||
this.element.appendChild(this.g);
|
this.element.appendChild(this.g);
|
||||||
|
|
||||||
// filters
|
// filters
|
||||||
|
@ -517,7 +625,7 @@ svg.sim.grayscale {
|
||||||
}
|
}
|
||||||
|
|
||||||
// head
|
// head
|
||||||
this.head = <SVGGElement>svg.child(this.g, "g", {});
|
this.head = <SVGGElement>svg.child(this.g, "g", {class: "sim-head"});
|
||||||
svg.child(this.head, "circle", { cx: 258, cy: 75, r: 100, fill: "transparent" })
|
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", "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"));
|
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",
|
"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",
|
"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"
|
"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
|
// 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" }));
|
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")
|
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"));
|
||||||
svg.title(this.pins[5], "P5, BUTTON A")
|
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"));
|
||||||
svg.title(this.pins[6], "P6, LED Col 9")
|
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"));
|
||||||
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.forEach((p, i) => svg.hydrate(p, {title: pinTitles[i]}));
|
||||||
|
|
||||||
this.pinGradients = this.pins.map((pin, i) => {
|
this.pinGradients = this.pins.map((pin, i) => {
|
||||||
let gid = "gradient-pin-" + i
|
let gid = "gradient-pin-" + i
|
||||||
|
@ -571,6 +660,7 @@ svg.sim.grayscale {
|
||||||
})
|
})
|
||||||
|
|
||||||
this.pinTexts = [67, 165, 275].map(x => <SVGTextElement>svg.child(this.g, "text", { class: "sim-text-pin", x: x, y: 345 }));
|
this.pinTexts = [67, 165, 275].map(x => <SVGTextElement>svg.child(this.g, "text", { class: "sim-text-pin", x: x, y: 345 }));
|
||||||
|
|
||||||
this.buttonsOuter = []; this.buttons = [];
|
this.buttonsOuter = []; this.buttons = [];
|
||||||
|
|
||||||
const outerBtn = (left: number, top: number) => {
|
const outerBtn = (left: number, top: number) => {
|
||||||
|
@ -578,7 +668,7 @@ svg.sim.grayscale {
|
||||||
const btnw = 56.2;
|
const btnw = 56.2;
|
||||||
const btnn = 6;
|
const btnn = 6;
|
||||||
const btnnm = 10
|
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);
|
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, "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 });
|
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;
|
let tiltDecayer = 0;
|
||||||
this.element.addEventListener(pointerEvents.move, (ev: MouseEvent) => {
|
this.element.addEventListener(pointerEvents.move, (ev: MouseEvent) => {
|
||||||
let state = this.board;
|
let state = this.board;
|
||||||
if (!state.accelerometer.isActive) return;
|
if (!state.accelerometerState.accelerometer.isActive) return;
|
||||||
|
|
||||||
if (tiltDecayer) {
|
if (tiltDecayer) {
|
||||||
clearInterval(tiltDecayer);
|
clearInterval(tiltDecayer);
|
||||||
|
@ -635,18 +725,18 @@ svg.sim.grayscale {
|
||||||
let z2 = 1023 * 1023 - x * x - y * y;
|
let z2 = 1023 * 1023 - x * x - y * y;
|
||||||
let z = Math.floor((z2 > 0 ? -1 : 1) * Math.sqrt(Math.abs(z2)));
|
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();
|
this.updateTilt();
|
||||||
}, false);
|
}, false);
|
||||||
this.element.addEventListener(pointerEvents.leave, (ev: MouseEvent) => {
|
this.element.addEventListener(pointerEvents.leave, (ev: MouseEvent) => {
|
||||||
let state = this.board;
|
let state = this.board;
|
||||||
if (!state.accelerometer.isActive) return;
|
if (!state.accelerometerState.accelerometer.isActive) return;
|
||||||
|
|
||||||
if (!tiltDecayer) {
|
if (!tiltDecayer) {
|
||||||
tiltDecayer = setInterval(() => {
|
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);
|
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);
|
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));
|
let accz = -Math.sqrt(Math.max(0, 1023 * 1023 - accx * accx - accy * accy));
|
||||||
if (Math.abs(accx) <= 24 && Math.abs(accy) <= 24) {
|
if (Math.abs(accx) <= 24 && Math.abs(accy) <= 24) {
|
||||||
|
@ -656,20 +746,20 @@ svg.sim.grayscale {
|
||||||
accy = 0;
|
accy = 0;
|
||||||
accz = -1023;
|
accz = -1023;
|
||||||
}
|
}
|
||||||
state.accelerometer.update(accx, accy, accz);
|
state.accelerometerState.accelerometer.update(accx, accy, accz);
|
||||||
this.updateTilt();
|
this.updateTilt();
|
||||||
}, 50)
|
}, 50)
|
||||||
}
|
}
|
||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
this.pins.forEach((pin, index) => {
|
this.pins.forEach((pin, index) => {
|
||||||
if (!this.board.pins[index]) return;
|
if (!this.board.edgeConnectorState.pins[index]) return;
|
||||||
let pt = this.element.createSVGPoint();
|
let pt = this.element.createSVGPoint();
|
||||||
svg.buttonEvents(pin,
|
svg.buttonEvents(pin,
|
||||||
// move
|
// move
|
||||||
ev => {
|
ev => {
|
||||||
let state = this.board;
|
let state = this.board;
|
||||||
let pin = state.pins[index];
|
let pin = state.edgeConnectorState.pins[index];
|
||||||
let svgpin = this.pins[index];
|
let svgpin = this.pins[index];
|
||||||
if (pin.mode & PinFlags.Input) {
|
if (pin.mode & PinFlags.Input) {
|
||||||
let cursor = svg.cursorPoint(pt, this.element, ev);
|
let cursor = svg.cursorPoint(pt, this.element, ev);
|
||||||
|
@ -681,7 +771,7 @@ svg.sim.grayscale {
|
||||||
// start
|
// start
|
||||||
ev => {
|
ev => {
|
||||||
let state = this.board;
|
let state = this.board;
|
||||||
let pin = state.pins[index];
|
let pin = state.edgeConnectorState.pins[index];
|
||||||
let svgpin = this.pins[index];
|
let svgpin = this.pins[index];
|
||||||
svg.addClass(svgpin, "touched");
|
svg.addClass(svgpin, "touched");
|
||||||
if (pin.mode & PinFlags.Input) {
|
if (pin.mode & PinFlags.Input) {
|
||||||
|
@ -694,7 +784,7 @@ svg.sim.grayscale {
|
||||||
// stop
|
// stop
|
||||||
(ev: MouseEvent) => {
|
(ev: MouseEvent) => {
|
||||||
let state = this.board;
|
let state = this.board;
|
||||||
let pin = state.pins[index];
|
let pin = state.edgeConnectorState.pins[index];
|
||||||
let svgpin = this.pins[index];
|
let svgpin = this.pins[index];
|
||||||
svg.removeClass(svgpin, "touched");
|
svg.removeClass(svgpin, "touched");
|
||||||
this.updatePin(pin, index);
|
this.updatePin(pin, index);
|
||||||
|
@ -704,70 +794,73 @@ svg.sim.grayscale {
|
||||||
this.pins.slice(0, 3).forEach((btn, index) => {
|
this.pins.slice(0, 3).forEach((btn, index) => {
|
||||||
btn.addEventListener(pointerEvents.down, ev => {
|
btn.addEventListener(pointerEvents.down, ev => {
|
||||||
let state = this.board;
|
let state = this.board;
|
||||||
state.pins[index].touched = true;
|
state.edgeConnectorState.pins[index].touched = true;
|
||||||
this.updatePin(state.pins[index], index);
|
this.updatePin(state.edgeConnectorState.pins[index], index);
|
||||||
})
|
})
|
||||||
btn.addEventListener(pointerEvents.leave, ev => {
|
btn.addEventListener(pointerEvents.leave, ev => {
|
||||||
let state = this.board;
|
let state = this.board;
|
||||||
state.pins[index].touched = false;
|
state.edgeConnectorState.pins[index].touched = false;
|
||||||
this.updatePin(state.pins[index], index);
|
this.updatePin(state.edgeConnectorState.pins[index], index);
|
||||||
})
|
})
|
||||||
btn.addEventListener(pointerEvents.up, ev => {
|
btn.addEventListener(pointerEvents.up, ev => {
|
||||||
let state = this.board;
|
let state = this.board;
|
||||||
state.pins[index].touched = false;
|
state.edgeConnectorState.pins[index].touched = false;
|
||||||
this.updatePin(state.pins[index], index);
|
this.updatePin(state.edgeConnectorState.pins[index], index);
|
||||||
this.board.bus.queue(state.pins[index].id, DAL.MICROBIT_BUTTON_EVT_UP);
|
this.board.bus.queue(state.edgeConnectorState.pins[index].id, DAL.MICROBIT_BUTTON_EVT_UP);
|
||||||
this.board.bus.queue(state.pins[index].id, DAL.MICROBIT_BUTTON_EVT_CLICK);
|
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) => {
|
this.buttonsOuter.slice(0, 2).forEach((btn, index) => {
|
||||||
btn.addEventListener(pointerEvents.down, ev => {
|
btn.addEventListener(pointerEvents.down, ev => {
|
||||||
let state = this.board;
|
let state = this.board;
|
||||||
state.buttons[index].pressed = true;
|
stateButtons[index].pressed = true;
|
||||||
svg.fill(this.buttons[index], this.props.theme.buttonDown);
|
svg.fill(this.buttons[index], this.props.theme.buttonDown);
|
||||||
})
|
})
|
||||||
btn.addEventListener(pointerEvents.leave, ev => {
|
btn.addEventListener(pointerEvents.leave, ev => {
|
||||||
let state = this.board;
|
let state = this.board;
|
||||||
state.buttons[index].pressed = false;
|
stateButtons[index].pressed = false;
|
||||||
svg.fill(this.buttons[index], this.props.theme.buttonUp);
|
svg.fill(this.buttons[index], this.props.theme.buttonUp);
|
||||||
})
|
})
|
||||||
btn.addEventListener(pointerEvents.up, ev => {
|
btn.addEventListener(pointerEvents.up, ev => {
|
||||||
let state = this.board;
|
let state = this.board;
|
||||||
state.buttons[index].pressed = false;
|
stateButtons[index].pressed = false;
|
||||||
svg.fill(this.buttons[index], this.props.theme.buttonUp);
|
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(stateButtons[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_CLICK);
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
this.buttonsOuter[2].addEventListener(pointerEvents.down, ev => {
|
this.buttonsOuter[2].addEventListener(pointerEvents.down, ev => {
|
||||||
let state = this.board;
|
let state = this.board;
|
||||||
state.buttons[0].pressed = true;
|
stateButtons[0].pressed = true;
|
||||||
state.buttons[1].pressed = true;
|
stateButtons[1].pressed = true;
|
||||||
state.buttons[2].pressed = true;
|
stateButtons[2].pressed = true;
|
||||||
svg.fill(this.buttons[0], this.props.theme.buttonDown);
|
svg.fill(this.buttons[0], this.props.theme.buttonDown);
|
||||||
svg.fill(this.buttons[1], this.props.theme.buttonDown);
|
svg.fill(this.buttons[1], this.props.theme.buttonDown);
|
||||||
svg.fill(this.buttons[2], this.props.theme.buttonDown);
|
svg.fill(this.buttons[2], this.props.theme.buttonDown);
|
||||||
})
|
})
|
||||||
this.buttonsOuter[2].addEventListener(pointerEvents.leave, ev => {
|
this.buttonsOuter[2].addEventListener(pointerEvents.leave, ev => {
|
||||||
let state = this.board;
|
let state = this.board;
|
||||||
state.buttons[0].pressed = false;
|
stateButtons[0].pressed = false;
|
||||||
state.buttons[1].pressed = false;
|
stateButtons[1].pressed = false;
|
||||||
state.buttons[2].pressed = false;
|
stateButtons[2].pressed = false;
|
||||||
svg.fill(this.buttons[0], this.props.theme.buttonUp);
|
svg.fill(this.buttons[0], this.props.theme.buttonUp);
|
||||||
svg.fill(this.buttons[1], this.props.theme.buttonUp);
|
svg.fill(this.buttons[1], this.props.theme.buttonUp);
|
||||||
svg.fill(this.buttons[2], this.props.theme.virtualButtonUp);
|
svg.fill(this.buttons[2], this.props.theme.virtualButtonUp);
|
||||||
})
|
})
|
||||||
this.buttonsOuter[2].addEventListener(pointerEvents.up, ev => {
|
this.buttonsOuter[2].addEventListener(pointerEvents.up, ev => {
|
||||||
let state = this.board;
|
let state = this.board;
|
||||||
state.buttons[0].pressed = false;
|
stateButtons[0].pressed = false;
|
||||||
state.buttons[1].pressed = false;
|
stateButtons[1].pressed = false;
|
||||||
state.buttons[2].pressed = false;
|
stateButtons[2].pressed = false;
|
||||||
svg.fill(this.buttons[0], this.props.theme.buttonUp);
|
svg.fill(this.buttons[0], this.props.theme.buttonUp);
|
||||||
svg.fill(this.buttons[1], this.props.theme.buttonUp);
|
svg.fill(this.buttons[1], this.props.theme.buttonUp);
|
||||||
svg.fill(this.buttons[2], this.props.theme.virtualButtonUp);
|
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(stateButtons[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_CLICK);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
203
sim/public/instructions.html
Normal file
203
sim/public/instructions.html
Normal file
|
@ -0,0 +1,203 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en" data-framework="typescript">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Assembly Instructions</title>
|
||||||
|
<style>
|
||||||
|
|
||||||
|
svg {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocklyText, .ace_editor {
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocklyText, .ace_editor {
|
||||||
|
font-size: 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocklyTreeLabel {
|
||||||
|
font-size: 1.25rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocklyCheckbox {
|
||||||
|
fill: #ff3030 !important;
|
||||||
|
text-shadow: 0px 0px 6px #f00;
|
||||||
|
font-size: 17pt !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui.card .blocklyPreview {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: calc(100% - 1em);
|
||||||
|
max-height: calc(100% - 1em);
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
code.lang-config, code.lang-package { display:none; }
|
||||||
|
|
||||||
|
code.lang-blocks::before,
|
||||||
|
code.lang-sig::before,
|
||||||
|
code.lang-block::before,
|
||||||
|
code.lang-shuffle::before,
|
||||||
|
code.lang-sim::before,
|
||||||
|
code.lang-cards::before,
|
||||||
|
code.lang-namespaces::before,
|
||||||
|
code.lang-codecard::before {
|
||||||
|
content: "...";
|
||||||
|
position: absolute;
|
||||||
|
top: calc(50% - 0.5em);
|
||||||
|
left: calc(50% - 5em);
|
||||||
|
}
|
||||||
|
|
||||||
|
code.lang-blocks,
|
||||||
|
code.lang-sig,
|
||||||
|
code.lang-block,
|
||||||
|
code.lang-shuffle,
|
||||||
|
code.lang-sim,
|
||||||
|
code.lang-cards,
|
||||||
|
code.lang-namespaces,
|
||||||
|
code.lang-codecard {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style type="text/css">
|
||||||
|
@import "/cdn/semantic.css";
|
||||||
|
@import "/cdn/icons.css";
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font-family: "Lucida Console", Monaco, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
/*undo semantic UI*/
|
||||||
|
box-sizing: content-box;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
/*TODO: Share CSS with main webpage*/
|
||||||
|
|
||||||
|
.organization {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: normal;
|
||||||
|
color: rgba(0, 0, 0, 0.87);
|
||||||
|
font-family: 'Segoe UI', 'Helvetica Neue', Arial, Helvetica, sans-serif;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#front-panel .board-svg {
|
||||||
|
position: absolute;
|
||||||
|
left: 1rem;
|
||||||
|
width: 140px;
|
||||||
|
top: 8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#proj-title {
|
||||||
|
top: 20px;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#proj-code {
|
||||||
|
width: 140px;
|
||||||
|
height: 200px;
|
||||||
|
position: absolute;
|
||||||
|
right: 1rem;
|
||||||
|
top: 8rem;
|
||||||
|
}
|
||||||
|
#proj-code-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#proj-code-spinner {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-panel svg {
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto;
|
||||||
|
left: inherit;
|
||||||
|
bottom: -7px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id='loading' class="ui active inverted dimmer">
|
||||||
|
<div class="ui large loader"></div>
|
||||||
|
</div>
|
||||||
|
<!-- start Mixpanel --><script type="text/javascript">(function(e,b){if(!b.__SV){var a,f,i,g;window.mixpanel=b;b._i=[];b.init=function(a,e,d){function f(b,h){var a=h.split(".");2==a.length&&(b=b[a[0]],h=a[1]);b[h]=function(){b.push([h].concat(Array.prototype.slice.call(arguments,0)))}}var c=b;"undefined"!==typeof d?c=b[d]=[]:d="mixpanel";c.people=c.people||[];c.toString=function(b){var a="mixpanel";"mixpanel"!==d&&(a+="."+d);b||(a+=" (stub)");return a};c.people.toString=function(){return c.toString(1)+".people (stub)"};i="disable time_event track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config reset people.set people.set_once people.increment people.append people.union people.track_charge people.clear_charges people.delete_user".split(" ");
|
||||||
|
for(g=0;g<i.length;g++)f(c,i[g]);b._i.push([a,e,d])};b.__SV=1.2;a=e.createElement("script");a.type="text/javascript";a.async=!0;a.src="undefined"!==typeof MIXPANEL_CUSTOM_LIB_URL?MIXPANEL_CUSTOM_LIB_URL:"file:"===e.location.protocol&&"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js".match(/^\/\//)?"https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js":"//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js";f=e.getElementsByTagName("script")[0];f.parentNode.insertBefore(a,f)}})(document,window.mixpanel||[]);
|
||||||
|
mixpanel.init("762fef19c053a0ea4cec43d2fecae76e");</script><!-- end Mixpanel -->
|
||||||
|
<script>
|
||||||
|
// This line gets patched up by the cloud
|
||||||
|
var pxtConfig = null;
|
||||||
|
</script>
|
||||||
|
<script type="text/javascript" src="/cdn/lzma/lzma_worker-min.js"></script>
|
||||||
|
<script type="text/javascript" src="/cdn/marked/marked.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/cdn/jquery.js"></script>
|
||||||
|
<script type="text/javascript" src="/cdn/typescript.js"></script>
|
||||||
|
<script type="text/javascript" src="/cdn/blockly/blockly_compressed.js"></script>
|
||||||
|
<script type="text/javascript" src="/cdn/blockly/blocks_compressed.js"></script>
|
||||||
|
<script type="text/javascript" src="/cdn/blockly/msg/js/en.js"></script>
|
||||||
|
<script type="text/javascript" src="/cdn/pxtlib.js"></script>
|
||||||
|
<script type="text/javascript" src="/cdn/pxtblocks.js"></script>
|
||||||
|
<script type="text/javascript" src="/cdn/pxtsim.js"></script>
|
||||||
|
<script type="text/javascript" src="/cdn/pxtrunner.js"></script>
|
||||||
|
<script type="text/javascript" src="/cdn/semantic.js"></script>
|
||||||
|
<script type="text/javascript" src="/embed.js"></script>
|
||||||
|
<script type="text/javascript" src="/sim/sim.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
(function () {
|
||||||
|
ksRunnerReady(function() {
|
||||||
|
var orgLogo = pxt.appTarget.appTheme.organizationLogo;
|
||||||
|
if (orgLogo)
|
||||||
|
$('#front-panel').append(
|
||||||
|
$('<img/>').attr('class', 'organization').attr('src', orgLogo)
|
||||||
|
);
|
||||||
|
var loading = document.getElementById('loading');
|
||||||
|
pxsim.instructions.drawInstructions();
|
||||||
|
$(loading).hide();
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="front-panel" class="instr-panel">
|
||||||
|
<h1 id="proj-title"></h1>
|
||||||
|
|
||||||
|
<!--TODO: extract real code snapshot from PXT -->
|
||||||
|
<div id="proj-code">
|
||||||
|
<i id="proj-code-spinner" class="spinner loading icon"></i>
|
||||||
|
<div id="proj-code-container">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
218
sim/simlib.ts
Normal file
218
sim/simlib.ts
Normal file
|
@ -0,0 +1,218 @@
|
||||||
|
/// <reference path="../node_modules/pxt-core/typings/bluebird/bluebird.d.ts"/>
|
||||||
|
/// <reference path="../node_modules/pxt-core/built/pxtsim.d.ts"/>
|
||||||
|
/// <reference path="../libs/microbit/dal.d.ts"/>
|
||||||
|
|
||||||
|
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<SVGSVGElement>,
|
||||||
|
scaleUnit1: number,
|
||||||
|
el2: SVGAndSize<SVGSVGElement>,
|
||||||
|
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 = <SVGSVGElement>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 = <SVGGElement>svg.child(host, "g");
|
||||||
|
host.appendChild(a.el);
|
||||||
|
host.appendChild(b.el);
|
||||||
|
let over = <SVGGElement>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<T> {
|
||||||
|
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 = <SVGTextElement>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<string> = {
|
||||||
|
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<T extends SVGElement> {
|
||||||
|
el: T,
|
||||||
|
y: number,
|
||||||
|
x: number,
|
||||||
|
w: number,
|
||||||
|
h: number
|
||||||
|
};
|
||||||
|
export type SVGElAndSize = SVGAndSize<SVGElement>;
|
||||||
|
|
||||||
|
export const PIN_DIST = 15;
|
||||||
|
|
||||||
|
export interface BoardView {
|
||||||
|
getView(): SVGAndSize<SVGSVGElement>;
|
||||||
|
getCoord(pinNm: string): Coord;
|
||||||
|
getPinDist(): number;
|
||||||
|
highlightPin(pinNm: string): void;
|
||||||
|
}
|
||||||
|
}
|
711
sim/state.ts
711
sim/state.ts
|
@ -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<EventQueue<number>> = {};
|
|
||||||
|
|
||||||
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<number>(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);
|
|
||||||
(<Board>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(<SimulatorRadioPacketMessage>{
|
|
||||||
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(<SimulatorEventBusMessage>{
|
|
||||||
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<void> {
|
|
||||||
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 = <SimulatorEventBusMessage>msg;
|
|
||||||
this.bus.queue(ev.id, ev.eventid, ev.value);
|
|
||||||
break;
|
|
||||||
case "serial":
|
|
||||||
this.serialIn.push((<SimulatorSerialMessage>msg).data || "");
|
|
||||||
break;
|
|
||||||
case "radiopacket":
|
|
||||||
let packet = <SimulatorRadioPacketMessage>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(<SimulatorSerialMessage>{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
389
sim/state/accelerometer.ts
Normal file
389
sim/state/accelerometer.ts
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
sim/state/buttonpair.ts
Normal file
41
sim/state/buttonpair.ts
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
sim/state/compass.ts
Normal file
22
sim/state/compass.ts
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
178
sim/state/edgeconnector.ts
Normal file
178
sim/state/edgeconnector.ts
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
357
sim/state/ledmatrix.ts
Normal file
357
sim/state/ledmatrix.ts
Normal file
|
@ -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<boolean>((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;
|
||||||
|
}
|
||||||
|
}
|
17
sim/state/lightsensor.ts
Normal file
17
sim/state/lightsensor.ts
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
228
sim/state/misc.ts
Normal file
228
sim/state/misc.ts
Normal file
|
@ -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 {
|
||||||
|
(<any>window).AudioContext = (<any>window).AudioContext || (<any>window).webkitAudioContext;
|
||||||
|
if ((<any>window).AudioContext) {
|
||||||
|
try {
|
||||||
|
// this call my crash.
|
||||||
|
// SyntaxError: audio resources unavailable for AudioContext construction
|
||||||
|
return new (<any>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<EventQueue<number>> = {};
|
||||||
|
|
||||||
|
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<number>(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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
57
sim/state/neopixel.ts
Normal file
57
sim/state/neopixel.ts
Normal file
|
@ -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 = <Uint8Array[]>(<any>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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
158
sim/state/radio.ts
Normal file
158
sim/state/radio.ts
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
(<DalBoard>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(<SimulatorRadioPacketMessage>{
|
||||||
|
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(<SimulatorEventBusMessage>{
|
||||||
|
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 <string>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();
|
||||||
|
}
|
||||||
|
}
|
54
sim/state/serial.ts
Normal file
54
sim/state/serial.ts
Normal file
|
@ -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(<SimulatorSerialMessage>{
|
||||||
|
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?
|
||||||
|
}
|
||||||
|
}
|
18
sim/state/thermometer.ts
Normal file
18
sim/state/thermometer.ts
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
181
sim/visuals/boardhost.ts
Normal file
181
sim/visuals/boardhost.ts
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
namespace pxsim.visuals {
|
||||||
|
export interface BoardHostOpts {
|
||||||
|
state: DalBoard,
|
||||||
|
boardDef: BoardDefinition,
|
||||||
|
cmpsList?: string[],
|
||||||
|
cmpDefs: Map<PartDefinition>,
|
||||||
|
fnArgs: any,
|
||||||
|
forceBreadboard?: boolean,
|
||||||
|
maxWidth?: string,
|
||||||
|
maxHeight?: string
|
||||||
|
wireframe?: boolean
|
||||||
|
}
|
||||||
|
export class BoardHost {
|
||||||
|
private components: IBoardComponent<any>[] = [];
|
||||||
|
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 = <SVGStyleElement>svg.child(this.view, "style", {});
|
||||||
|
this.defs = <SVGDefsElement>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");
|
||||||
|
(<any>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 = (<BBLoc>loc).rowCol;
|
||||||
|
coord = this.getBBCoord(rowCol);
|
||||||
|
} else {
|
||||||
|
let pinNm = (<BoardLoc>loc).pin;
|
||||||
|
coord = this.getPinCoord(pinNm);
|
||||||
|
}
|
||||||
|
if (!coord) {
|
||||||
|
console.error("Unknown location: " + name)
|
||||||
|
return [0, 0];
|
||||||
|
}
|
||||||
|
return coord;
|
||||||
|
}
|
||||||
|
|
||||||
|
public addComponent(cmpDesc: CmpInst): IBoardComponent<any> {
|
||||||
|
let cmp: IBoardComponent<any> = 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 = <BBRowCol>[`${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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
649
sim/visuals/breadboard.ts
Normal file
649
sim/visuals/breadboard.ts
Normal file
|
@ -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 = <SVGGElement>svg.elt("g");
|
||||||
|
let colIdxOffset = opts.colStartIdx || 0;
|
||||||
|
let rowIdxOffset = opts.rowStartIdx || 0;
|
||||||
|
let copyArr = <T>(arr: T[]): T[] => arr ? arr.slice(0, arr.length) : [];
|
||||||
|
let removeAll = <T>(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<Map<GridPin>> = {};
|
||||||
|
private rowColToLbls: Map<Map<GridLabel[]>> = {};
|
||||||
|
|
||||||
|
constructor(opts: BreadboardOpts) {
|
||||||
|
this.buildDom();
|
||||||
|
|
||||||
|
if (opts.wireframe)
|
||||||
|
svg.addClass(this.bb, "sim-bb-outline");
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateLocation(x: number, y: number) {
|
||||||
|
svg.hydrate(this.bb, {
|
||||||
|
x: `${x}px`,
|
||||||
|
y: `${y}px`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPin(row: string, col: string): GridPin {
|
||||||
|
let colToPin = this.rowColToPin[row];
|
||||||
|
if (!colToPin)
|
||||||
|
return null;
|
||||||
|
let pin = colToPin[col];
|
||||||
|
if (!pin)
|
||||||
|
return null;
|
||||||
|
return pin;
|
||||||
|
}
|
||||||
|
public getCoord(rowCol: BBRowCol): Coord {
|
||||||
|
let [row, col] = rowCol;
|
||||||
|
let pin = this.getPin(row, col);
|
||||||
|
if (!pin)
|
||||||
|
return null;
|
||||||
|
return [pin.cx, pin.cy];
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPinDist() {
|
||||||
|
return PIN_DIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildDom() {
|
||||||
|
this.bb = <SVGSVGElement>svg.elt("svg", {
|
||||||
|
"version": "1.0",
|
||||||
|
"viewBox": `0 0 ${WIDTH} ${HEIGHT}`,
|
||||||
|
"class": `sim-bb`,
|
||||||
|
"width": WIDTH + "px",
|
||||||
|
"height": HEIGHT + "px",
|
||||||
|
});
|
||||||
|
this.styleEl = <SVGStyleElement>svg.child(this.bb, "style", {});
|
||||||
|
this.styleEl.textContent += BREADBOARD_CSS;
|
||||||
|
this.defs = <SVGDefsElement>svg.child(this.bb, "defs", {});
|
||||||
|
|
||||||
|
//background
|
||||||
|
svg.child(this.bb, "rect", { class: "sim-bb-background", width: WIDTH, height: HEIGHT, rx: BACKGROUND_ROUNDING, ry: BACKGROUND_ROUNDING});
|
||||||
|
|
||||||
|
//mid channel
|
||||||
|
let channelGid = "sim-bb-channel-grad";
|
||||||
|
let channelGrad = <SVGLinearGradientElement>svg.elt("linearGradient")
|
||||||
|
svg.hydrate(channelGrad, { id: channelGid, x1: "0%", y1: "0%", x2: "0%", y2: "100%" });
|
||||||
|
this.defs.appendChild(channelGrad);
|
||||||
|
let channelDark = "#AAA";
|
||||||
|
let channelLight = "#CCC";
|
||||||
|
let stop1 = svg.child(channelGrad, "stop", { offset: "0%", style: `stop-color: ${channelDark};` })
|
||||||
|
let stop2 = svg.child(channelGrad, "stop", { offset: "20%", style: `stop-color: ${channelLight};` })
|
||||||
|
let stop3 = svg.child(channelGrad, "stop", { offset: "80%", style: `stop-color: ${channelLight};` })
|
||||||
|
let stop4 = svg.child(channelGrad, "stop", { offset: "100%", style: `stop-color: ${channelDark};` })
|
||||||
|
|
||||||
|
const mkChannel = (cy: number, h: number, cls?: string) => {
|
||||||
|
let channel = svg.child(this.bb, "rect", { class: `sim-bb-channel ${cls || ""}`, y: cy - h / 2, width: WIDTH, height: h});
|
||||||
|
channel.setAttribute("fill", `url(#${channelGid})`);
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
mkChannel(BAR_HEIGHT + MID_HEIGHT / 2, CHANNEL_HEIGHT, "sim-bb-mid-channel");
|
||||||
|
mkChannel(BAR_HEIGHT, SMALL_CHANNEL_HEIGHT);
|
||||||
|
mkChannel(BAR_HEIGHT + MID_HEIGHT, SMALL_CHANNEL_HEIGHT);
|
||||||
|
|
||||||
|
//-----pins
|
||||||
|
const getMidTopOrBot = (rowIdx: number) => rowIdx < MID_ROWS / 2.0 ? "b" : "t";
|
||||||
|
const getBarTopOrBot = (colIdx: number) => colIdx < POWER_COLS / 2.0 ? "b" : "t";
|
||||||
|
const alphabet = "abcdefghij".split("").reverse();
|
||||||
|
const getColName = (colIdx: number) => `${colIdx + 1}`;
|
||||||
|
const getMidRowName = (rowIdx: number) => alphabet[rowIdx];
|
||||||
|
const getMidGroupName = (rowIdx: number, colIdx: number) => {
|
||||||
|
let botOrTop = getMidTopOrBot(rowIdx);
|
||||||
|
let colNm = getColName(colIdx);
|
||||||
|
return `${botOrTop}${colNm}`;
|
||||||
|
};
|
||||||
|
const getBarRowName = (rowIdx: number) => rowIdx === 0 ? "-" : "+";
|
||||||
|
const getBarGroupName = (rowIdx: number, colIdx: number) => {
|
||||||
|
let botOrTop = getBarTopOrBot(colIdx);
|
||||||
|
let rowName = getBarRowName(rowIdx);
|
||||||
|
return `${rowName}${botOrTop}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
//mid grid
|
||||||
|
let midGridRes = mkGrid({
|
||||||
|
xOffset: MID_GRID_X,
|
||||||
|
yOffset: MID_GRID_Y,
|
||||||
|
rowCount: MID_ROWS,
|
||||||
|
colCount: MID_COLS,
|
||||||
|
pinDist: PIN_DIST,
|
||||||
|
mkPin: mkBBPin,
|
||||||
|
mkHoverPin: mkBBHoverPin,
|
||||||
|
getRowName: getMidRowName,
|
||||||
|
getColName: getColName,
|
||||||
|
getGroupName: getMidGroupName,
|
||||||
|
rowIdxsWithGap: MID_ROW_GAPS,
|
||||||
|
});
|
||||||
|
let midGridG = midGridRes.g;
|
||||||
|
this.allPins = this.allPins.concat(midGridRes.allPins);
|
||||||
|
|
||||||
|
//bot bar
|
||||||
|
let botBarGridRes = mkGrid({
|
||||||
|
xOffset: BAR_BOT_GRID_X,
|
||||||
|
yOffset: BAR_BOT_GRID_Y,
|
||||||
|
rowCount: BAR_ROWS,
|
||||||
|
colCount: BAR_COLS,
|
||||||
|
pinDist: PIN_DIST,
|
||||||
|
mkPin: mkBBPin,
|
||||||
|
mkHoverPin: mkBBHoverPin,
|
||||||
|
getRowName: getBarRowName,
|
||||||
|
getColName: getColName,
|
||||||
|
getGroupName: getBarGroupName,
|
||||||
|
colIdxsWithGap: BAR_COL_GAPS,
|
||||||
|
});
|
||||||
|
let botBarGridG = botBarGridRes.g;
|
||||||
|
this.allPins = this.allPins.concat(botBarGridRes.allPins);
|
||||||
|
|
||||||
|
//top bar
|
||||||
|
let topBarGridRes = mkGrid({
|
||||||
|
xOffset: BAR_TOP_GRID_X,
|
||||||
|
yOffset: BAR_TOP_GRID_Y,
|
||||||
|
rowCount: BAR_ROWS,
|
||||||
|
colCount: BAR_COLS,
|
||||||
|
colStartIdx: BAR_COLS,
|
||||||
|
pinDist: PIN_DIST,
|
||||||
|
mkPin: mkBBPin,
|
||||||
|
mkHoverPin: mkBBHoverPin,
|
||||||
|
getRowName: getBarRowName,
|
||||||
|
getColName: getColName,
|
||||||
|
getGroupName: getBarGroupName,
|
||||||
|
colIdxsWithGap: BAR_COL_GAPS.map(g => g + BAR_COLS),
|
||||||
|
});
|
||||||
|
let topBarGridG = topBarGridRes.g;
|
||||||
|
this.allPins = this.allPins.concat(topBarGridRes.allPins);
|
||||||
|
|
||||||
|
//tooltip
|
||||||
|
this.allPins.forEach(pin => {
|
||||||
|
let {el, row, col, hoverEl} = pin
|
||||||
|
let title = `(${row},${col})`;
|
||||||
|
svg.hydrate(el, {title: title});
|
||||||
|
svg.hydrate(hoverEl, {title: title});
|
||||||
|
})
|
||||||
|
|
||||||
|
//catalog pins
|
||||||
|
this.allPins.forEach(pin => {
|
||||||
|
let colToPin = this.rowColToPin[pin.row];
|
||||||
|
if (!colToPin)
|
||||||
|
colToPin = this.rowColToPin[pin.row] = {};
|
||||||
|
colToPin[pin.col] = pin;
|
||||||
|
})
|
||||||
|
|
||||||
|
//-----labels
|
||||||
|
const mkBBLabelAtPin = (row: string, col: string, xOffset: number, yOffset: number, txt: string, group?: string): GridLabel => {
|
||||||
|
let size = PIN_LBL_SIZE;
|
||||||
|
let rotation = LBL_ROTATION;
|
||||||
|
let loc = this.getCoord([row, col]);
|
||||||
|
let [cx, cy] = loc;
|
||||||
|
let t = mkBBLabel(cx + xOffset, cy + yOffset, size, rotation, txt, group);
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
//columns
|
||||||
|
for (let colIdx = 0; colIdx < MID_COLS; colIdx++) {
|
||||||
|
let colNm = getColName(colIdx);
|
||||||
|
//top
|
||||||
|
let rowTIdx = 0;
|
||||||
|
let rowTNm = getMidRowName(rowTIdx);
|
||||||
|
let groupT = getMidGroupName(rowTIdx, colIdx);
|
||||||
|
let lblT = mkBBLabelAtPin(rowTNm, colNm, 0, -PIN_DIST, colNm, groupT);
|
||||||
|
this.allLabels.push(lblT);
|
||||||
|
//bottom
|
||||||
|
let rowBIdx = MID_ROWS - 1;
|
||||||
|
let rowBNm = getMidRowName(rowBIdx);
|
||||||
|
let groupB = getMidGroupName(rowBIdx, colIdx);
|
||||||
|
let lblB = mkBBLabelAtPin(rowBNm, colNm, 0, +PIN_DIST, colNm, groupB);
|
||||||
|
this.allLabels.push(lblB);
|
||||||
|
}
|
||||||
|
//rows
|
||||||
|
for (let rowIdx = 0; rowIdx < MID_ROWS; rowIdx++) {
|
||||||
|
let rowNm = getMidRowName(rowIdx);
|
||||||
|
//top
|
||||||
|
let colTIdx = 0;
|
||||||
|
let colTNm = getColName(colTIdx);
|
||||||
|
let lblT = mkBBLabelAtPin(rowNm, colTNm, -PIN_DIST, 0, rowNm);
|
||||||
|
this.allLabels.push(lblT);
|
||||||
|
//top
|
||||||
|
let colBIdx = MID_COLS - 1;
|
||||||
|
let colBNm = getColName(colBIdx);
|
||||||
|
let lblB = mkBBLabelAtPin(rowNm, colBNm, +PIN_DIST, 0, rowNm);
|
||||||
|
this.allLabels.push(lblB);
|
||||||
|
}
|
||||||
|
|
||||||
|
//+- labels
|
||||||
|
let botPowerLabels = [
|
||||||
|
//BL
|
||||||
|
mkBBLabel(0 + POWER_LBL_OFFSET + MINUS_LBL_EXTRA_OFFSET, BAR_HEIGHT + MID_HEIGHT + POWER_LBL_OFFSET, MINUS_LBL_SIZE, LBL_ROTATION, `-`, getBarGroupName(0, 0), [`sim-bb-blue`]),
|
||||||
|
mkBBLabel(0 + POWER_LBL_OFFSET, BAR_HEIGHT + MID_HEIGHT + BAR_HEIGHT - POWER_LBL_OFFSET, PLUS_LBL_SIZE, LBL_ROTATION, `+`, getBarGroupName(1, 0), [`sim-bb-red`]),
|
||||||
|
//BR
|
||||||
|
mkBBLabel(WIDTH - POWER_LBL_OFFSET + MINUS_LBL_EXTRA_OFFSET, BAR_HEIGHT + MID_HEIGHT + POWER_LBL_OFFSET, MINUS_LBL_SIZE, LBL_ROTATION, `-`, getBarGroupName(0, BAR_COLS - 1), [`sim-bb-blue`]),
|
||||||
|
mkBBLabel(WIDTH - POWER_LBL_OFFSET, BAR_HEIGHT + MID_HEIGHT + BAR_HEIGHT - POWER_LBL_OFFSET, PLUS_LBL_SIZE, LBL_ROTATION, `+`, getBarGroupName(1, BAR_COLS - 1), [`sim-bb-red`]),
|
||||||
|
];
|
||||||
|
this.allLabels = this.allLabels.concat(botPowerLabels);
|
||||||
|
let topPowerLabels = [
|
||||||
|
//TL
|
||||||
|
mkBBLabel(0 + POWER_LBL_OFFSET + MINUS_LBL_EXTRA_OFFSET, 0 + POWER_LBL_OFFSET, MINUS_LBL_SIZE, LBL_ROTATION, `-`, getBarGroupName(0, BAR_COLS), [`sim-bb-blue`]),
|
||||||
|
mkBBLabel(0 + POWER_LBL_OFFSET, BAR_HEIGHT - POWER_LBL_OFFSET, PLUS_LBL_SIZE, LBL_ROTATION, `+`, getBarGroupName(1, BAR_COLS), [`sim-bb-red`]),
|
||||||
|
//TR
|
||||||
|
mkBBLabel(WIDTH - POWER_LBL_OFFSET + MINUS_LBL_EXTRA_OFFSET, 0 + POWER_LBL_OFFSET, MINUS_LBL_SIZE, LBL_ROTATION, `-`, getBarGroupName(0, POWER_COLS - 1), [`sim-bb-blue`]),
|
||||||
|
mkBBLabel(WIDTH - POWER_LBL_OFFSET, BAR_HEIGHT - POWER_LBL_OFFSET, PLUS_LBL_SIZE, LBL_ROTATION, `+`, getBarGroupName(1, POWER_COLS - 1), [`sim-bb-red`]),
|
||||||
|
];
|
||||||
|
this.allLabels = this.allLabels.concat(topPowerLabels);
|
||||||
|
|
||||||
|
//catalog lbls
|
||||||
|
let lblNmToLbls: Map<GridLabel[]> = {};
|
||||||
|
this.allLabels.forEach(lbl => {
|
||||||
|
let {el, txt} = lbl;
|
||||||
|
let lbls = lblNmToLbls[txt] = lblNmToLbls[txt] || []
|
||||||
|
lbls.push(lbl);
|
||||||
|
});
|
||||||
|
const isPowerPin = (pin: GridPin) => pin.row === "-" || pin.row === "+";
|
||||||
|
this.allPins.forEach(pin => {
|
||||||
|
let {row, col, group} = pin;
|
||||||
|
let colToLbls = this.rowColToLbls[row] || (this.rowColToLbls[row] = {});
|
||||||
|
let lbls = colToLbls[col] || (colToLbls[col] = []);
|
||||||
|
if (isPowerPin(pin)) {
|
||||||
|
//power pins
|
||||||
|
let isBot = Number(col) <= BAR_COLS;
|
||||||
|
if (isBot)
|
||||||
|
botPowerLabels.filter(l => l.group == pin.group).forEach(l => lbls.push(l));
|
||||||
|
else
|
||||||
|
topPowerLabels.filter(l => l.group == pin.group).forEach(l => lbls.push(l));
|
||||||
|
} else {
|
||||||
|
//mid pins
|
||||||
|
let rowLbls = lblNmToLbls[row];
|
||||||
|
rowLbls.forEach(l => lbls.push(l));
|
||||||
|
let colLbls = lblNmToLbls[col];
|
||||||
|
colLbls.forEach(l => lbls.push(l));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
//-----blue & red lines
|
||||||
|
const lnLen = BAR_GRID_WIDTH + PIN_DIST * 1.5;
|
||||||
|
const lnThickness = PIN_DIST / 5.0;
|
||||||
|
const lnYOff = PIN_DIST * 0.6;
|
||||||
|
const lnXOff = (lnLen - BAR_GRID_WIDTH) / 2.0;
|
||||||
|
const mkPowerLine = (x: number, y: number, group: string, cls: string): BBBar => {
|
||||||
|
let ln = <SVGRectElement>svg.elt("rect");
|
||||||
|
svg.hydrate(ln, {
|
||||||
|
class: `sim-bb-bar ${cls}`,
|
||||||
|
x: x,
|
||||||
|
y: y - lnThickness / 2.0,
|
||||||
|
width: lnLen,
|
||||||
|
height: lnThickness});
|
||||||
|
let bar: BBBar = {el: ln, group: group};
|
||||||
|
return bar;
|
||||||
|
}
|
||||||
|
let barLines = [
|
||||||
|
//top
|
||||||
|
mkPowerLine(BAR_BOT_GRID_X - lnXOff, BAR_BOT_GRID_Y - lnYOff, getBarGroupName(0, POWER_COLS - 1), "sim-bb-blue"),
|
||||||
|
mkPowerLine(BAR_BOT_GRID_X - lnXOff, BAR_BOT_GRID_Y + PIN_DIST + lnYOff, getBarGroupName(1, POWER_COLS - 1), "sim-bb-red"),
|
||||||
|
//bot
|
||||||
|
mkPowerLine(BAR_TOP_GRID_X - lnXOff, BAR_TOP_GRID_Y - lnYOff, getBarGroupName(0, 0), "sim-bb-blue"),
|
||||||
|
mkPowerLine(BAR_TOP_GRID_X - lnXOff, BAR_TOP_GRID_Y + PIN_DIST + lnYOff, getBarGroupName(1, 0), "sim-bb-red"),
|
||||||
|
];
|
||||||
|
this.allPowerBars = this.allPowerBars.concat(barLines);
|
||||||
|
//attach power bars
|
||||||
|
this.allPowerBars.forEach(b => this.bb.appendChild(b.el));
|
||||||
|
|
||||||
|
//-----electrically connected groups
|
||||||
|
//make groups
|
||||||
|
let allGrpNms = this.allPins.map(p => p.group).filter((g, i, a) => a.indexOf(g) == i);
|
||||||
|
let groups: SVGGElement[] = allGrpNms.map(grpNm => {
|
||||||
|
let g = <SVGGElement>svg.elt("g");
|
||||||
|
return g;
|
||||||
|
});
|
||||||
|
groups.forEach(g => svg.addClass(g, "sim-bb-pin-group"));
|
||||||
|
groups.forEach((g, i) => svg.addClass(g, `group-${allGrpNms[i]}`));
|
||||||
|
let grpNmToGroup: Map<SVGGElement> = {};
|
||||||
|
allGrpNms.forEach((g, i) => grpNmToGroup[g] = groups[i]);
|
||||||
|
//group pins and add connecting wire
|
||||||
|
let grpNmToPins: Map<GridPin[]> = {};
|
||||||
|
this.allPins.forEach((p, i) => {
|
||||||
|
let g = p.group;
|
||||||
|
let pins = grpNmToPins[g] || (grpNmToPins[g] = []);
|
||||||
|
pins.push(p);
|
||||||
|
});
|
||||||
|
//connecting wire
|
||||||
|
allGrpNms.forEach(grpNm => {
|
||||||
|
let pins = grpNmToPins[grpNm];
|
||||||
|
let [xs, ys] = [pins.map(p => p.cx), pins.map(p => p.cy)];
|
||||||
|
let minFn = (arr: number[]) => arr.reduce((a, b) => a < b ? a : b);
|
||||||
|
let maxFn = (arr: number[]) => arr.reduce((a, b) => a > b ? a : b);
|
||||||
|
let [minX, maxX, minY, maxY] = [minFn(xs), maxFn(xs), minFn(ys), maxFn(ys)];
|
||||||
|
let wire = svg.elt("rect");
|
||||||
|
let width = Math.max(maxX - minX, 0.0001/*rects with no width aren't displayed*/);
|
||||||
|
let height = Math.max(maxY - minY, 0.0001);
|
||||||
|
svg.hydrate(wire, {x: minX, y: minY, width: width, height: height});
|
||||||
|
svg.addClass(wire, "sim-bb-group-wire")
|
||||||
|
let g = grpNmToGroup[grpNm];
|
||||||
|
g.appendChild(wire);
|
||||||
|
});
|
||||||
|
//group pins
|
||||||
|
this.allPins.forEach(p => {
|
||||||
|
let g = grpNmToGroup[p.group];
|
||||||
|
g.appendChild(p.el);
|
||||||
|
g.appendChild(p.hoverEl);
|
||||||
|
})
|
||||||
|
//group lbls
|
||||||
|
let miscLblGroup = <SVGGElement>svg.elt("g");
|
||||||
|
svg.hydrate(miscLblGroup, {class: "sim-bb-group-misc"});
|
||||||
|
groups.push(miscLblGroup);
|
||||||
|
this.allLabels.forEach(l => {
|
||||||
|
if (l.group) {
|
||||||
|
let g = grpNmToGroup[l.group];
|
||||||
|
g.appendChild(l.el);
|
||||||
|
g.appendChild(l.hoverEl);
|
||||||
|
} else {
|
||||||
|
miscLblGroup.appendChild(l.el);
|
||||||
|
miscLblGroup.appendChild(l.hoverEl);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
//attach to bb
|
||||||
|
groups.forEach(g => this.bb.appendChild(g)); //attach to breadboard
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSVGAndSize(): SVGAndSize<SVGSVGElement> {
|
||||||
|
return {el: this.bb, y: 0, x: 0, w: WIDTH, h: HEIGHT};
|
||||||
|
}
|
||||||
|
|
||||||
|
public highlightLoc(rowCol: BBRowCol) {
|
||||||
|
let [row, col] = rowCol;
|
||||||
|
let pin = this.rowColToPin[row][col];
|
||||||
|
let {cx, cy} = pin;
|
||||||
|
let lbls = this.rowColToLbls[row][col];
|
||||||
|
const highlightLbl = (lbl: GridLabel) => {
|
||||||
|
svg.addClass(lbl.el, "highlight");
|
||||||
|
svg.addClass(lbl.hoverEl, "highlight");
|
||||||
|
};
|
||||||
|
lbls.forEach(highlightLbl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
204
sim/visuals/buttonpair.ts
Normal file
204
sim/visuals/buttonpair.ts
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
/// <reference path="../../node_modules/pxt-core/typings/bluebird/bluebird.d.ts"/>
|
||||||
|
/// <reference path="../../node_modules/pxt-core/built/pxtsim.d.ts"/>
|
||||||
|
/// <reference path="../../libs/microbit/dal.d.ts"/>
|
||||||
|
|
||||||
|
namespace pxsim.visuals {
|
||||||
|
export function mkBtnSvg(xy: Coord): SVGAndSize<SVGGElement> {
|
||||||
|
let [innerCls, outerCls] = ["sim-button", "sim-button-outer"];
|
||||||
|
const tabSize = PIN_DIST / 2.5;
|
||||||
|
const pegR = PIN_DIST / 5;
|
||||||
|
const btnR = PIN_DIST * .8;
|
||||||
|
const pegMargin = PIN_DIST / 8;
|
||||||
|
const plateR = PIN_DIST / 12;
|
||||||
|
|
||||||
|
const pegOffset = pegMargin + pegR;
|
||||||
|
let [x, y] = xy;
|
||||||
|
const left = x - tabSize / 2;
|
||||||
|
const top = y - tabSize / 2;
|
||||||
|
const plateH = 3 * PIN_DIST - tabSize;
|
||||||
|
const plateW = 2 * PIN_DIST + tabSize;
|
||||||
|
const plateL = left;
|
||||||
|
const plateT = top + tabSize;
|
||||||
|
const btnCX = plateL + plateW / 2;
|
||||||
|
const btnCY = plateT + plateH / 2;
|
||||||
|
|
||||||
|
let btng = <SVGGElement>svg.elt("g");
|
||||||
|
//tabs
|
||||||
|
const mkTab = (x: number, y: number) => {
|
||||||
|
svg.child(btng, "rect", { class: "sim-button-tab", x: x, y: y, width: tabSize, height: tabSize})
|
||||||
|
}
|
||||||
|
mkTab(left, top);
|
||||||
|
mkTab(left + 2 * PIN_DIST, top);
|
||||||
|
mkTab(left, top + 3 * PIN_DIST);
|
||||||
|
mkTab(left + 2 * PIN_DIST, top + 3 * PIN_DIST);
|
||||||
|
|
||||||
|
//plate
|
||||||
|
svg.child(btng, "rect", { class: outerCls, x: plateL, y: plateT, rx: plateR, ry: plateR, width: plateW, height: plateH });
|
||||||
|
|
||||||
|
//pegs
|
||||||
|
const mkPeg = (x: number, y: number) => {
|
||||||
|
svg.child(btng, "circle", { class: "sim-button-nut", cx: x, cy: y, r: pegR });
|
||||||
|
}
|
||||||
|
mkPeg(plateL + pegOffset, plateT + pegOffset)
|
||||||
|
mkPeg(plateL + plateW - pegOffset, plateT + pegOffset)
|
||||||
|
mkPeg(plateL + pegOffset, plateT + plateH - pegOffset)
|
||||||
|
mkPeg(plateL + plateW - pegOffset, plateT + plateH - pegOffset)
|
||||||
|
|
||||||
|
//inner btn
|
||||||
|
let innerBtn = svg.child(btng, "circle", { class: innerCls, cx: btnCX, cy: btnCY, r: btnR });
|
||||||
|
|
||||||
|
//return
|
||||||
|
return { el: btng, y: top, x: left, w: plateW, h: plateH + 2 * tabSize };
|
||||||
|
}
|
||||||
|
export const BUTTON_PAIR_STYLE = `
|
||||||
|
.sim-button {
|
||||||
|
pointer-events: none;
|
||||||
|
fill: #000;
|
||||||
|
}
|
||||||
|
.sim-button-outer:active ~ .sim-button,
|
||||||
|
.sim-button-virtual:active {
|
||||||
|
fill: #FFA500;
|
||||||
|
}
|
||||||
|
.sim-button-outer {
|
||||||
|
cursor: pointer;
|
||||||
|
fill: #979797;
|
||||||
|
}
|
||||||
|
.sim-button-outer:hover {
|
||||||
|
stroke:gray;
|
||||||
|
stroke-width: ${PIN_DIST / 5}px;
|
||||||
|
}
|
||||||
|
.sim-button-nut {
|
||||||
|
fill:#000;
|
||||||
|
pointer-events:none;
|
||||||
|
}
|
||||||
|
.sim-button-nut:hover {
|
||||||
|
stroke:${PIN_DIST / 15}px solid #704A4A;
|
||||||
|
}
|
||||||
|
.sim-button-tab {
|
||||||
|
fill:#FFF;
|
||||||
|
pointer-events:none;
|
||||||
|
}
|
||||||
|
.sim-button-virtual {
|
||||||
|
cursor: pointer;
|
||||||
|
fill: rgba(255, 255, 255, 0.6);
|
||||||
|
stroke: rgba(255, 255, 255, 1);
|
||||||
|
stroke-width: ${PIN_DIST / 5}px;
|
||||||
|
}
|
||||||
|
.sim-button-virtual:hover {
|
||||||
|
stroke: rgba(128, 128, 128, 1);
|
||||||
|
}
|
||||||
|
.sim-text-virtual {
|
||||||
|
fill: #000;
|
||||||
|
pointer-events:none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export class ButtonPairView implements IBoardComponent<ButtonPairState> {
|
||||||
|
public element: SVGElement;
|
||||||
|
public defs: SVGElement[];
|
||||||
|
public style = BUTTON_PAIR_STYLE;
|
||||||
|
private state: ButtonPairState;
|
||||||
|
private bus: EventBus;
|
||||||
|
private aBtn: SVGGElement;
|
||||||
|
private bBtn: SVGGElement;
|
||||||
|
private abBtn: SVGGElement;
|
||||||
|
|
||||||
|
public init(bus: EventBus, state: ButtonPairState) {
|
||||||
|
this.state = state;
|
||||||
|
this.bus = bus;
|
||||||
|
this.defs = [];
|
||||||
|
this.element = this.mkBtns();
|
||||||
|
this.updateState();
|
||||||
|
this.attachEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
public moveToCoord(xy: Coord) {
|
||||||
|
let btnWidth = PIN_DIST * 3;
|
||||||
|
let [x, y] = xy;
|
||||||
|
translateEl(this.aBtn, [x, y])
|
||||||
|
translateEl(this.bBtn, [x + btnWidth, y])
|
||||||
|
translateEl(this.abBtn, [x + PIN_DIST * 1.5, y + PIN_DIST * 4])
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateState() {
|
||||||
|
let stateBtns = [this.state.aBtn, this.state.bBtn, this.state.abBtn];
|
||||||
|
let svgBtns = [this.aBtn, this.bBtn, this.abBtn];
|
||||||
|
|
||||||
|
if (this.state.usesButtonAB && this.abBtn.style.visibility != "visible") {
|
||||||
|
this.abBtn.style.visibility = "visible";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateTheme() {}
|
||||||
|
|
||||||
|
private mkBtns() {
|
||||||
|
this.aBtn = mkBtnSvg([0, 0]).el;
|
||||||
|
this.bBtn = mkBtnSvg([0, 0]).el;
|
||||||
|
|
||||||
|
const mkVirtualBtn = () => {
|
||||||
|
const numPins = 2;
|
||||||
|
const w = PIN_DIST * 2.8;
|
||||||
|
const offset = (w - (numPins * PIN_DIST)) / 2;
|
||||||
|
const corner = PIN_DIST / 2;
|
||||||
|
const cx = 0 - offset + w / 2;
|
||||||
|
const cy = cx;
|
||||||
|
const txtSize = PIN_DIST * 1.3;
|
||||||
|
const x = -offset;
|
||||||
|
const y = -offset;
|
||||||
|
const txtXOff = PIN_DIST / 7;
|
||||||
|
const txtYOff = PIN_DIST / 10;
|
||||||
|
|
||||||
|
let btng = <SVGGElement>svg.elt("g");
|
||||||
|
let btn = svg.child(btng, "rect", { class: "sim-button-virtual", x: x, y: y, rx: corner, ry: corner, width: w, height: w});
|
||||||
|
let btnTxt = mkTxt(cx + txtXOff, cy + txtYOff, txtSize, 0, "A+B");
|
||||||
|
svg.addClass(btnTxt, "sim-text")
|
||||||
|
svg.addClass(btnTxt, "sim-text-virtual");
|
||||||
|
btng.appendChild(btnTxt);
|
||||||
|
|
||||||
|
return btng;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.abBtn = mkVirtualBtn();
|
||||||
|
this.abBtn.style.visibility = "hidden";
|
||||||
|
|
||||||
|
let el = svg.elt("g");
|
||||||
|
svg.addClass(el, "sim-buttonpair")
|
||||||
|
el.appendChild(this.aBtn);
|
||||||
|
el.appendChild(this.bBtn);
|
||||||
|
el.appendChild(this.abBtn);
|
||||||
|
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
private attachEvents() {
|
||||||
|
let btnStates = [this.state.aBtn, this.state.bBtn];
|
||||||
|
let btnSvgs = [this.aBtn, this.bBtn];
|
||||||
|
btnSvgs.forEach((btn, index) => {
|
||||||
|
btn.addEventListener(pointerEvents.down, ev => {
|
||||||
|
btnStates[index].pressed = true;
|
||||||
|
})
|
||||||
|
btn.addEventListener(pointerEvents.leave, ev => {
|
||||||
|
btnStates[index].pressed = false;
|
||||||
|
})
|
||||||
|
btn.addEventListener(pointerEvents.up, ev => {
|
||||||
|
btnStates[index].pressed = false;
|
||||||
|
this.bus.queue(btnStates[index].id, DAL.MICROBIT_BUTTON_EVT_UP);
|
||||||
|
this.bus.queue(btnStates[index].id, DAL.MICROBIT_BUTTON_EVT_CLICK);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
let updateBtns = (s: boolean) => {
|
||||||
|
btnStates.forEach(b => b.pressed = s)
|
||||||
|
};
|
||||||
|
this.abBtn.addEventListener(pointerEvents.down, ev => {
|
||||||
|
updateBtns(true);
|
||||||
|
})
|
||||||
|
this.abBtn.addEventListener(pointerEvents.leave, ev => {
|
||||||
|
updateBtns(false);
|
||||||
|
})
|
||||||
|
this.abBtn.addEventListener(pointerEvents.up, ev => {
|
||||||
|
updateBtns(false);
|
||||||
|
this.bus.queue(this.state.abBtn.id, DAL.MICROBIT_BUTTON_EVT_UP);
|
||||||
|
this.bus.queue(this.state.abBtn.id, DAL.MICROBIT_BUTTON_EVT_CLICK);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
515
sim/visuals/genericboard.ts
Normal file
515
sim/visuals/genericboard.ts
Normal file
|
@ -0,0 +1,515 @@
|
||||||
|
/// <reference path="../../node_modules/pxt-core/typings/bluebird/bluebird.d.ts"/>
|
||||||
|
/// <reference path="../../node_modules/pxt-core/built/pxtsim.d.ts"/>
|
||||||
|
/// <reference path="../../libs/microbit/dal.d.ts"/>
|
||||||
|
|
||||||
|
namespace pxsim.visuals {
|
||||||
|
const svg = pxsim.svg;
|
||||||
|
|
||||||
|
export interface IBoardSvgProps {
|
||||||
|
runtime: pxsim.Runtime;
|
||||||
|
boardDef: BoardDefinition;
|
||||||
|
disableTilt?: boolean;
|
||||||
|
activeComponents: string[];
|
||||||
|
fnArgs?: any;
|
||||||
|
componentDefinitions: Map<PartDefinition>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VIEW_WIDTH = 498;
|
||||||
|
export const VIEW_HEIGHT = 725;
|
||||||
|
const TOP_MARGIN = 20;
|
||||||
|
const MID_MARGIN = 40;
|
||||||
|
const BOT_MARGIN = 20;
|
||||||
|
const PIN_LBL_SIZE = PIN_DIST * 0.7;
|
||||||
|
const PIN_LBL_HOVER_SIZE = PIN_LBL_SIZE * 1.5;
|
||||||
|
const SQUARE_PIN_WIDTH = PIN_DIST * 0.66666;
|
||||||
|
const SQUARE_PIN_HOVER_WIDTH = PIN_DIST * 0.66666 + PIN_DIST / 3.0;
|
||||||
|
|
||||||
|
export type ComputedBoardDimensions = {
|
||||||
|
scaleFn: (n: number) => number,
|
||||||
|
height: number,
|
||||||
|
width: number,
|
||||||
|
xOff: number,
|
||||||
|
yOff: number
|
||||||
|
};
|
||||||
|
export function getBoardDimensions(vis: BoardImageDefinition): ComputedBoardDimensions {
|
||||||
|
let scaleFn = (n: number) => n * (PIN_DIST / vis.pinDist);
|
||||||
|
let width = scaleFn(vis.width);
|
||||||
|
return {
|
||||||
|
scaleFn: scaleFn,
|
||||||
|
height: scaleFn(vis.height),
|
||||||
|
width: width,
|
||||||
|
xOff: (VIEW_WIDTH - width) / 2.0,
|
||||||
|
yOff: TOP_MARGIN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BOARD_SYTLE = `
|
||||||
|
.noselect {
|
||||||
|
-webkit-touch-callout: none; /* iOS Safari */
|
||||||
|
-webkit-user-select: none; /* Chrome/Safari/Opera */
|
||||||
|
-khtml-user-select: none; /* Konqueror */
|
||||||
|
-moz-user-select: none; /* Firefox */
|
||||||
|
-ms-user-select: none; /* Internet Explorer/Edge */
|
||||||
|
user-select: none; /* Non-prefixed version, currently
|
||||||
|
not supported by any browser */
|
||||||
|
}
|
||||||
|
svg.sim.grayscale {
|
||||||
|
-moz-filter: grayscale(1);
|
||||||
|
-webkit-filter: grayscale(1);
|
||||||
|
filter: grayscale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sim-text {
|
||||||
|
font-family:"Lucida Console", Monaco, monospace;
|
||||||
|
font-size:25px;
|
||||||
|
fill:#fff;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.sim-board-pin {
|
||||||
|
fill:#999;
|
||||||
|
stroke:#000;
|
||||||
|
stroke-width:${PIN_DIST / 3.0}px;
|
||||||
|
}
|
||||||
|
.sim-board-pin-lbl {
|
||||||
|
fill: #333;
|
||||||
|
}
|
||||||
|
.gray-cover {
|
||||||
|
fill:#FFF;
|
||||||
|
opacity: 0.7;
|
||||||
|
stroke-width:0;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.sim-board-pin-hover {
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: all;
|
||||||
|
stroke-width:${PIN_DIST / 6.0}px;
|
||||||
|
}
|
||||||
|
.sim-board-pin-hover:hover {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
.sim-board-pin-lbl {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.sim-board-outline .sim-board-pin-lbl {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
.sim-board-pin-lbl {
|
||||||
|
fill: #555;
|
||||||
|
}
|
||||||
|
.sim-board-pin-lbl-hover {
|
||||||
|
fill: red;
|
||||||
|
}
|
||||||
|
.sim-board-outline .sim-board-pin-lbl-hover {
|
||||||
|
fill: black;
|
||||||
|
}
|
||||||
|
.sim-board-pin-lbl,
|
||||||
|
.sim-board-pin-lbl-hover {
|
||||||
|
font-family:"Lucida Console", Monaco, monospace;
|
||||||
|
pointer-events: all;
|
||||||
|
stroke-width: 0;
|
||||||
|
}
|
||||||
|
.sim-board-pin-lbl-hover {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.sim-board-outline .sim-board-pin-hover:hover + .sim-board-pin-lbl,
|
||||||
|
.sim-board-pin-lbl.highlight {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.sim-board-outline .sim-board-pin-hover:hover + * + .sim-board-pin-lbl-hover,
|
||||||
|
.sim-board-pin-lbl-hover.highlight {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
/* Graying out */
|
||||||
|
.grayed .sim-board-pin-lbl:not(.highlight) {
|
||||||
|
fill: #AAA;
|
||||||
|
}
|
||||||
|
.grayed .sim-board-pin:not(.highlight) {
|
||||||
|
fill:#BBB;
|
||||||
|
stroke:#777;
|
||||||
|
}
|
||||||
|
.grayed .gray-cover {
|
||||||
|
visibility: inherit;
|
||||||
|
}
|
||||||
|
.grayed .sim-cmp:not(.notgrayed) {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
/* Highlighting */
|
||||||
|
.sim-board-pin-lbl.highlight {
|
||||||
|
fill: #000;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.sim-board-pin.highlight {
|
||||||
|
fill:#999;
|
||||||
|
stroke:#000;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
let nextBoardId = 0;
|
||||||
|
export class GenericBoardSvg /*TODO: implements BoardView*/ {
|
||||||
|
public hostElement: SVGSVGElement;
|
||||||
|
private style: SVGStyleElement;
|
||||||
|
private defs: SVGDefsElement;
|
||||||
|
private g: SVGGElement;
|
||||||
|
public board: pxsim.DalBoard;
|
||||||
|
public background: SVGElement;
|
||||||
|
private components: IBoardComponent<any>[];
|
||||||
|
public breadboard: Breadboard;
|
||||||
|
private underboard: SVGGElement;
|
||||||
|
public boardDef: BoardDefinition;
|
||||||
|
private boardDim: ComputedBoardDimensions;
|
||||||
|
public componentDefs: Map<PartDefinition>;
|
||||||
|
private boardEdges: number[];
|
||||||
|
private id: number;
|
||||||
|
public bbX: number;
|
||||||
|
public bbY: number;
|
||||||
|
private boardTopEdge: number;
|
||||||
|
private boardBotEdge: number;
|
||||||
|
private wireFactory: WireFactory;
|
||||||
|
//truth
|
||||||
|
private allPins: GridPin[] = [];
|
||||||
|
private allLabels: GridLabel[] = [];
|
||||||
|
//cache
|
||||||
|
private pinNmToLbl: Map<GridLabel> = {};
|
||||||
|
private pinNmToPin: Map<GridPin> = {};
|
||||||
|
|
||||||
|
constructor(public props: IBoardSvgProps) {
|
||||||
|
this.id = nextBoardId++;
|
||||||
|
this.boardDef = props.boardDef;
|
||||||
|
this.boardDim = getBoardDimensions(<BoardImageDefinition>this.boardDef.visual);
|
||||||
|
this.board = this.props.runtime.board as pxsim.DalBoard;
|
||||||
|
this.board.updateView = () => this.updateState();
|
||||||
|
this.hostElement = <SVGSVGElement>svg.elt("svg")
|
||||||
|
svg.hydrate(this.hostElement, {
|
||||||
|
"version": "1.0",
|
||||||
|
"viewBox": `0 0 ${VIEW_WIDTH} ${VIEW_HEIGHT}`,
|
||||||
|
"enable-background": `new 0 0 ${VIEW_WIDTH} ${VIEW_HEIGHT}`,
|
||||||
|
"class": `sim sim-board-id-${this.id}`,
|
||||||
|
"x": "0px",
|
||||||
|
"y": "0px"
|
||||||
|
});
|
||||||
|
this.style = <SVGStyleElement>svg.child(this.hostElement, "style", {});
|
||||||
|
this.style.textContent += BOARD_SYTLE;
|
||||||
|
this.defs = <SVGDefsElement>svg.child(this.hostElement, "defs", {});
|
||||||
|
this.g = <SVGGElement>svg.elt("g");
|
||||||
|
this.hostElement.appendChild(this.g);
|
||||||
|
this.underboard = <SVGGElement>svg.child(this.g, "g", {class: "sim-underboard"});
|
||||||
|
this.components = [];
|
||||||
|
this.componentDefs = props.componentDefinitions;
|
||||||
|
|
||||||
|
// breadboard
|
||||||
|
this.breadboard = new Breadboard({})
|
||||||
|
this.g.appendChild(this.breadboard.bb);
|
||||||
|
let bbSize = this.breadboard.getSVGAndSize();
|
||||||
|
let [bbWidth, bbHeight] = [bbSize.w, bbSize.h];
|
||||||
|
const bbX = (VIEW_WIDTH - bbWidth) / 2;
|
||||||
|
this.bbX = bbX;
|
||||||
|
const bbY = TOP_MARGIN + this.boardDim.height + MID_MARGIN;
|
||||||
|
this.bbY = bbY;
|
||||||
|
this.breadboard.updateLocation(bbX, bbY);
|
||||||
|
|
||||||
|
// edges
|
||||||
|
this.boardTopEdge = TOP_MARGIN;
|
||||||
|
this.boardBotEdge = TOP_MARGIN + this.boardDim.height;
|
||||||
|
this.boardEdges = [this.boardTopEdge, this.boardBotEdge, bbY, bbY + bbHeight]
|
||||||
|
|
||||||
|
this.wireFactory = new WireFactory(this.underboard, this.g, this.boardEdges, this.style, this.getLocCoord.bind(this));
|
||||||
|
|
||||||
|
this.buildDom();
|
||||||
|
|
||||||
|
this.updateTheme();
|
||||||
|
this.updateState();
|
||||||
|
|
||||||
|
let cmps = props.activeComponents;
|
||||||
|
if (cmps.length) {
|
||||||
|
let allocRes = allocateDefinitions({
|
||||||
|
boardDef: this.boardDef,
|
||||||
|
cmpDefs: this.componentDefs,
|
||||||
|
fnArgs: this.props.fnArgs,
|
||||||
|
getBBCoord: this.getBBCoord.bind(this),
|
||||||
|
cmpList: props.activeComponents,
|
||||||
|
});
|
||||||
|
this.addAll(allocRes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBoardPinCoord(pinNm: string): Coord {
|
||||||
|
let pin = this.pinNmToPin[pinNm];
|
||||||
|
if (!pin)
|
||||||
|
return null;
|
||||||
|
return [pin.cx, pin.cy];
|
||||||
|
}
|
||||||
|
private getBBCoord(rowCol: BBRowCol): Coord {
|
||||||
|
let bbCoord = this.breadboard.getCoord(rowCol);
|
||||||
|
if (!bbCoord)
|
||||||
|
return null;
|
||||||
|
let [x, y] = bbCoord;
|
||||||
|
return [x + this.bbX, y + this.bbY];
|
||||||
|
}
|
||||||
|
|
||||||
|
public getLocCoord(loc: Loc): Coord {
|
||||||
|
let coord: Coord;
|
||||||
|
if (loc.type === "breadboard") {
|
||||||
|
let rowCol = (<BBLoc>loc).rowCol;
|
||||||
|
coord = this.getBBCoord(rowCol);
|
||||||
|
} else {
|
||||||
|
let pinNm = (<BoardLoc>loc).pin;
|
||||||
|
coord = this.getBoardPinCoord(pinNm);
|
||||||
|
}
|
||||||
|
if (!coord) {
|
||||||
|
console.error("Unknown location: " + name)
|
||||||
|
return [0, 0];
|
||||||
|
}
|
||||||
|
return coord;
|
||||||
|
}
|
||||||
|
|
||||||
|
private mkGrayCover(x: number, y: number, w: number, h: number) {
|
||||||
|
let rect = <SVGRectElement>svg.elt("rect");
|
||||||
|
svg.hydrate(rect, {x: x, y: y, width: w, height: h, class: "gray-cover"});
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCmpClass = (type: string) => `sim-${type}-cmp`;
|
||||||
|
|
||||||
|
public addWire(inst: WireInst): Wire {
|
||||||
|
return this.wireFactory.addWire(inst.start, inst.end, inst.color);
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public addComponent(cmpDesc: CmpInst): IBoardComponent<any> {
|
||||||
|
let cmp: IBoardComponent<any> = null;
|
||||||
|
if (typeof cmpDesc.visual === "string") {
|
||||||
|
let builtinVisual = cmpDesc.visual as string;
|
||||||
|
let cnstr = builtinComponentSimVisual[builtinVisual];
|
||||||
|
let stateFn = builtinComponentSimState[builtinVisual];
|
||||||
|
let state = stateFn(this.board);
|
||||||
|
cmp = cnstr();
|
||||||
|
cmp.init(this.board.bus, state, this.hostElement, cmpDesc.microbitPins, cmpDesc.otherArgs);
|
||||||
|
this.components.push(cmp);
|
||||||
|
this.g.appendChild(cmp.element);
|
||||||
|
if (cmp.defs)
|
||||||
|
cmp.defs.forEach(d => this.defs.appendChild(d));
|
||||||
|
this.style.textContent += cmp.style || "";
|
||||||
|
let rowCol = <BBRowCol>[`${cmpDesc.breadboardStartRow}`, `${cmpDesc.breadboardStartColumn}`];
|
||||||
|
let coord = this.getBBCoord(rowCol);
|
||||||
|
cmp.moveToCoord(coord);
|
||||||
|
let cls = this.getCmpClass(name);
|
||||||
|
svg.addClass(cmp.element, cls);
|
||||||
|
svg.addClass(cmp.element, "sim-cmp");
|
||||||
|
cmp.updateTheme();
|
||||||
|
cmp.updateState();
|
||||||
|
} else {
|
||||||
|
//TODO: adding generic components
|
||||||
|
}
|
||||||
|
return cmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateTheme() {
|
||||||
|
this.components.forEach(c => c.updateTheme());
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateState() {
|
||||||
|
let state = this.board;
|
||||||
|
if (!state) return;
|
||||||
|
|
||||||
|
this.components.forEach(c => c.updateState());
|
||||||
|
|
||||||
|
if (!runtime || runtime.dead) svg.addClass(this.hostElement, "grayscale");
|
||||||
|
else svg.removeClass(this.hostElement, "grayscale");
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildDom() {
|
||||||
|
|
||||||
|
// filters
|
||||||
|
let glow = svg.child(this.defs, "filter", { id: "filterglow", x: "-5%", y: "-5%", width: "120%", height: "120%" });
|
||||||
|
svg.child(glow, "feGaussianBlur", { stdDeviation: "5", result: "glow" });
|
||||||
|
let merge = svg.child(glow, "feMerge", {});
|
||||||
|
for (let i = 0; i < 3; ++i)
|
||||||
|
svg.child(merge, "feMergeNode", { in: "glow" })
|
||||||
|
|
||||||
|
// main board
|
||||||
|
this.background = svg.child(this.g, "image",
|
||||||
|
{ class: "sim-board", x: this.boardDim.xOff, y: this.boardDim.yOff, width: this.boardDim.width, height: this.boardDim.height,
|
||||||
|
"href": `${(<BoardImageDefinition>this.boardDef.visual).image}`});
|
||||||
|
let backgroundCover = this.mkGrayCover(this.boardDim.xOff, this.boardDim.yOff, this.boardDim.width, this.boardDim.height);
|
||||||
|
this.g.appendChild(backgroundCover);
|
||||||
|
|
||||||
|
// ----- pins
|
||||||
|
const mkSquarePin = (): SVGElAndSize => {
|
||||||
|
let el = svg.elt("rect");
|
||||||
|
let width = SQUARE_PIN_WIDTH;
|
||||||
|
svg.hydrate(el, {
|
||||||
|
class: "sim-board-pin",
|
||||||
|
width: width,
|
||||||
|
height: width,
|
||||||
|
});
|
||||||
|
return {el: el, w: width, h: width, x: 0, y: 0};
|
||||||
|
}
|
||||||
|
const mkSquareHoverPin = (): SVGElAndSize => {
|
||||||
|
let el = svg.elt("rect");
|
||||||
|
let width = SQUARE_PIN_HOVER_WIDTH;
|
||||||
|
svg.hydrate(el, {
|
||||||
|
class: "sim-board-pin-hover",
|
||||||
|
width: width,
|
||||||
|
height: width
|
||||||
|
});
|
||||||
|
return {el: el, w: width, h: width, x: 0, y: 0};
|
||||||
|
}
|
||||||
|
const mkPinBlockGrid = (pinBlock: PinBlockDefinition, blockIdx: number) => {
|
||||||
|
let xOffset = this.boardDim.xOff + this.boardDim.scaleFn(pinBlock.x) + PIN_DIST / 2.0;
|
||||||
|
let yOffset = this.boardDim.yOff + this.boardDim.scaleFn(pinBlock.y) + PIN_DIST / 2.0;
|
||||||
|
let rowCount = 1;
|
||||||
|
let colCount = pinBlock.labels.length;
|
||||||
|
let getColName = (colIdx: number) => pinBlock.labels[colIdx];
|
||||||
|
let getRowName = () => `${blockIdx + 1}`
|
||||||
|
let getGroupName = () => pinBlock.labels.join(" ");
|
||||||
|
let gridRes = mkGrid({
|
||||||
|
xOffset: xOffset,
|
||||||
|
yOffset: yOffset,
|
||||||
|
rowCount: rowCount,
|
||||||
|
colCount: colCount,
|
||||||
|
pinDist: PIN_DIST,
|
||||||
|
mkPin: mkSquarePin,
|
||||||
|
mkHoverPin: mkSquareHoverPin,
|
||||||
|
getRowName: getRowName,
|
||||||
|
getColName: getColName,
|
||||||
|
getGroupName: getGroupName,
|
||||||
|
});
|
||||||
|
let pins = gridRes.allPins;
|
||||||
|
let pinsG = gridRes.g;
|
||||||
|
svg.addClass(gridRes.g, "sim-board-pin-group");
|
||||||
|
return gridRes;
|
||||||
|
};
|
||||||
|
let pinBlocks = (<BoardImageDefinition>this.boardDef.visual).pinBlocks.map(mkPinBlockGrid);
|
||||||
|
pinBlocks.forEach(blk => blk.allPins.forEach(p => {
|
||||||
|
this.allPins.push(p);
|
||||||
|
}));
|
||||||
|
//tooltip
|
||||||
|
this.allPins.forEach(p => {
|
||||||
|
let tooltip = p.col;
|
||||||
|
svg.hydrate(p.el, {title: tooltip});
|
||||||
|
svg.hydrate(p.hoverEl, {title: tooltip});
|
||||||
|
});
|
||||||
|
//attach pins
|
||||||
|
this.allPins.forEach(p => {
|
||||||
|
this.g.appendChild(p.el);
|
||||||
|
this.g.appendChild(p.hoverEl);
|
||||||
|
});
|
||||||
|
//catalog pins
|
||||||
|
this.allPins.forEach(p => {
|
||||||
|
this.pinNmToPin[p.col] = p;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----- labels
|
||||||
|
const mkLabelTxtEl = (pinX: number, pinY: number, size: number, txt: string): SVGTextElement => {
|
||||||
|
//TODO: extract constants
|
||||||
|
let lblY: number;
|
||||||
|
let lblX: number;
|
||||||
|
let edges = [this.boardTopEdge, this.boardBotEdge];
|
||||||
|
let distFromTopBot = edges.map(e => Math.abs(e - pinY));
|
||||||
|
let closestEdgeIdx = distFromTopBot.reduce((pi, n, ni) => n < distFromTopBot[pi] ? ni : pi, 0);
|
||||||
|
let topEdge = closestEdgeIdx == 0;
|
||||||
|
if (topEdge) {
|
||||||
|
let lblLen = size * 0.25 * txt.length;
|
||||||
|
lblX = pinX;
|
||||||
|
lblY = pinY + 12 + lblLen;
|
||||||
|
} else {
|
||||||
|
let lblLen = size * 0.32 * txt.length;
|
||||||
|
lblX = pinX;
|
||||||
|
lblY = pinY - 11 - lblLen;
|
||||||
|
}
|
||||||
|
let el = mkTxt(lblX, lblY, size, -90, txt);
|
||||||
|
return el;
|
||||||
|
};
|
||||||
|
const mkLabel = (pinX: number, pinY: number, txt: string): GridLabel => {
|
||||||
|
let el = mkLabelTxtEl(pinX, pinY, PIN_LBL_SIZE, txt);
|
||||||
|
svg.addClass(el, "sim-board-pin-lbl");
|
||||||
|
let hoverEl = mkLabelTxtEl(pinX, pinY, PIN_LBL_HOVER_SIZE, txt);
|
||||||
|
svg.addClass(hoverEl, "sim-board-pin-lbl-hover");
|
||||||
|
let label: GridLabel = {el: el, hoverEl: hoverEl, txt: txt};
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
this.allLabels = this.allPins.map(p => {
|
||||||
|
return mkLabel(p.cx, p.cy, p.col);
|
||||||
|
});
|
||||||
|
//attach labels
|
||||||
|
this.allLabels.forEach(l => {
|
||||||
|
this.g.appendChild(l.el);
|
||||||
|
this.g.appendChild(l.hoverEl);
|
||||||
|
});
|
||||||
|
//catalog labels
|
||||||
|
this.allPins.forEach((pin, pinIdx) => {
|
||||||
|
let lbl = this.allLabels[pinIdx];
|
||||||
|
this.pinNmToLbl[pin.col] = lbl;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public highlightLoc(pinNm: string) {
|
||||||
|
let lbl = this.pinNmToLbl[pinNm];
|
||||||
|
let pin = this.pinNmToPin[pinNm];
|
||||||
|
if (lbl && pin) {
|
||||||
|
svg.addClass(lbl.el, "highlight");
|
||||||
|
svg.addClass(lbl.hoverEl, "highlight");
|
||||||
|
svg.addClass(pin.el, "highlight");
|
||||||
|
svg.addClass(pin.hoverEl, "highlight");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public highlightWire(wire: Wire) {
|
||||||
|
//underboard wires
|
||||||
|
wire.wires.forEach(e => {
|
||||||
|
(<any>e).style["visibility"] = "visible";
|
||||||
|
});
|
||||||
|
|
||||||
|
//un greyed out
|
||||||
|
[wire.end1, wire.end2].forEach(e => {
|
||||||
|
svg.addClass(e, "highlight");
|
||||||
|
});
|
||||||
|
wire.wires.forEach(e => {
|
||||||
|
svg.addClass(e, "highlight");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
sim/visuals/genericpart.ts
Normal file
16
sim/visuals/genericpart.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
|
||||||
|
namespace pxsim.visuals {
|
||||||
|
export class GenericComponentView implements IBoardComponent<any> {
|
||||||
|
public style: string;
|
||||||
|
public element: SVGElement;
|
||||||
|
defs: SVGElement[];
|
||||||
|
init(bus: EventBus, state: any, svgEl: SVGSVGElement, gpioPins: string[], otherArgs: string[]): void {
|
||||||
|
}
|
||||||
|
moveToCoord(xy: Coord): void {
|
||||||
|
}
|
||||||
|
updateState(): void {
|
||||||
|
}
|
||||||
|
updateTheme(): void {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
130
sim/visuals/ledmatrix.ts
Normal file
130
sim/visuals/ledmatrix.ts
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
/// <reference path="../../node_modules/pxt-core/typings/bluebird/bluebird.d.ts"/>
|
||||||
|
/// <reference path="../../node_modules/pxt-core/built/pxtsim.d.ts"/>
|
||||||
|
/// <reference path="../../libs/microbit/dal.d.ts"/>
|
||||||
|
|
||||||
|
namespace pxsim.visuals {
|
||||||
|
export function mkLedMatrixSvg(xy: Coord, rows: number, cols: number):
|
||||||
|
{el: SVGGElement, y: number, x: number, w: number, h: number, leds: SVGElement[], ledsOuter: SVGElement[], background: SVGElement} {
|
||||||
|
let result: {el: SVGGElement, y: number, x: number, w: number, h: number, leds: SVGElement[], ledsOuter: SVGElement[], background: SVGElement}
|
||||||
|
= {el: null, y: 0, x: 0, w: 0, h: 0, leds: [], ledsOuter: [], background: null};
|
||||||
|
result.el = <SVGGElement>svg.elt("g");
|
||||||
|
let width = cols * PIN_DIST;
|
||||||
|
let height = rows * PIN_DIST;
|
||||||
|
let ledRad = Math.round(PIN_DIST * .35);
|
||||||
|
let spacing = PIN_DIST;
|
||||||
|
let padding = (spacing - 2 * ledRad) / 2.0;
|
||||||
|
let [x, y] = xy;
|
||||||
|
let left = x - (ledRad + padding);
|
||||||
|
let top = y - (ledRad + padding);
|
||||||
|
result.x = left;
|
||||||
|
result.y = top;
|
||||||
|
result.w = width;
|
||||||
|
result.h = height;
|
||||||
|
result.background = svg.child(result.el, "rect", {class: "sim-display", x: left, y: top, width: width, height: height})
|
||||||
|
|
||||||
|
// ledsOuter
|
||||||
|
result.leds = [];
|
||||||
|
result.ledsOuter = [];
|
||||||
|
let hoverRad = ledRad * 1.2;
|
||||||
|
for (let i = 0; i < rows; ++i) {
|
||||||
|
let y = top + ledRad + i * spacing + padding;
|
||||||
|
for (let j = 0; j < cols; ++j) {
|
||||||
|
let x = left + ledRad + j * spacing + padding;
|
||||||
|
result.ledsOuter.push(svg.child(result.el, "circle", { class: "sim-led-back", cx: x, cy: y, r: ledRad }));
|
||||||
|
result.leds.push(svg.child(result.el, "circle", { class: "sim-led", cx: x, cy: y, r: hoverRad, title: `(${j},${i})` }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//default theme
|
||||||
|
svg.fill(result.background, defaultLedMatrixTheme.background);
|
||||||
|
svg.fills(result.leds, defaultLedMatrixTheme.ledOn);
|
||||||
|
svg.fills(result.ledsOuter, defaultLedMatrixTheme.ledOff);
|
||||||
|
|
||||||
|
//turn off LEDs
|
||||||
|
result.leds.forEach(l => (<SVGStylable><any>l).style.opacity = 0 + "");
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILedMatrixTheme {
|
||||||
|
background?: string;
|
||||||
|
ledOn?: string;
|
||||||
|
ledOff?: string;
|
||||||
|
}
|
||||||
|
export var defaultLedMatrixTheme: ILedMatrixTheme = {
|
||||||
|
background: "#000",
|
||||||
|
ledOn: "#ff5f5f",
|
||||||
|
ledOff: "#DDD",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LED_MATRIX_STYLE = `
|
||||||
|
.sim-led-back:hover {
|
||||||
|
stroke:#a0a0a0;
|
||||||
|
stroke-width:3px;
|
||||||
|
}
|
||||||
|
.sim-led:hover {
|
||||||
|
stroke:#ff7f7f;
|
||||||
|
stroke-width:3px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export class LedMatrixView implements IBoardComponent<LedMatrixState> {
|
||||||
|
private background: SVGElement;
|
||||||
|
private ledsOuter: SVGElement[];
|
||||||
|
private leds: SVGElement[];
|
||||||
|
private state: LedMatrixState;
|
||||||
|
private bus: EventBus;
|
||||||
|
public element: SVGElement;
|
||||||
|
public defs: SVGElement[];
|
||||||
|
private theme: ILedMatrixTheme;
|
||||||
|
|
||||||
|
private DRAW_SIZE = 8;
|
||||||
|
private ACTIVE_SIZE = 5;
|
||||||
|
|
||||||
|
public style = LED_MATRIX_STYLE;
|
||||||
|
|
||||||
|
public init(bus: EventBus, state: LedMatrixState) {
|
||||||
|
this.bus = bus;
|
||||||
|
this.state = state;
|
||||||
|
this.theme = defaultLedMatrixTheme;
|
||||||
|
this.defs = [];
|
||||||
|
this.element = this.buildDom();
|
||||||
|
}
|
||||||
|
|
||||||
|
public moveToCoord(xy: Coord) {
|
||||||
|
let [x, y] = xy;
|
||||||
|
translateEl(this.element, [x, y]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateTheme() {
|
||||||
|
svg.fill(this.background, this.theme.background);
|
||||||
|
svg.fills(this.leds, this.theme.ledOn);
|
||||||
|
svg.fills(this.ledsOuter, this.theme.ledOff);
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateState() {
|
||||||
|
let bw = this.state.displayMode == pxsim.DisplayMode.bw
|
||||||
|
let img = this.state.image;
|
||||||
|
this.leds.forEach((led, i) => {
|
||||||
|
let sel = (<SVGStylable><any>led)
|
||||||
|
let dx = i % this.DRAW_SIZE;
|
||||||
|
let dy = (i - dx) / this.DRAW_SIZE;
|
||||||
|
if (dx < this.ACTIVE_SIZE && dy < this.ACTIVE_SIZE) {
|
||||||
|
let j = dx + dy * this.ACTIVE_SIZE;
|
||||||
|
sel.style.opacity = ((bw ? img.data[j] > 0 ? 255 : 0 : img.data[j]) / 255.0) + "";
|
||||||
|
} else {
|
||||||
|
sel.style.opacity = 0 + "";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildDom() {
|
||||||
|
let res = mkLedMatrixSvg([0, 0], this.DRAW_SIZE, this.DRAW_SIZE);
|
||||||
|
let display = res.el;
|
||||||
|
this.background = res.background;
|
||||||
|
this.leds = res.leds;
|
||||||
|
this.ledsOuter = res.ledsOuter;
|
||||||
|
return display;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
256
sim/visuals/neopixel.ts
Normal file
256
sim/visuals/neopixel.ts
Normal file
|
@ -0,0 +1,256 @@
|
||||||
|
/// <reference path="../../node_modules/pxt-core/built/pxtsim.d.ts"/>
|
||||||
|
/// <reference path="../../libs/microbit/dal.d.ts"/>
|
||||||
|
/// <reference path="../../libs/microbit/shims.d.ts"/>
|
||||||
|
/// <reference path="../../libs/microbit/enums.d.ts"/>
|
||||||
|
/// <reference path="../state/neopixel.ts"/>
|
||||||
|
/// <reference path="../simlib.ts"/>
|
||||||
|
|
||||||
|
//TODO move to utils
|
||||||
|
namespace pxsim.visuals {
|
||||||
|
//expects rgb from 0,255, gives h in [0,360], s in [0, 100], l in [0, 100]
|
||||||
|
export function rgbToHsl(rgb: [number, number, number]): [number, number, number] {
|
||||||
|
let [r, g, b] = rgb;
|
||||||
|
let [r$, g$, b$] = [r / 255, g / 255, b / 255];
|
||||||
|
let cMin = Math.min(r$, g$, b$);
|
||||||
|
let cMax = Math.max(r$, g$, b$);
|
||||||
|
let cDelta = cMax - cMin;
|
||||||
|
let h: number, s: number, l: number;
|
||||||
|
let maxAndMin = cMax + cMin;
|
||||||
|
|
||||||
|
//lum
|
||||||
|
l = (maxAndMin / 2) * 100
|
||||||
|
|
||||||
|
if (cDelta === 0)
|
||||||
|
s = h = 0;
|
||||||
|
else {
|
||||||
|
//hue
|
||||||
|
if (cMax === r$)
|
||||||
|
h = 60 * (((g$ - b$) / cDelta) % 6);
|
||||||
|
else if (cMax === g$)
|
||||||
|
h = 60 * (((b$ - r$) / cDelta) + 2);
|
||||||
|
else if (cMax === b$)
|
||||||
|
h = 60 * (((r$ - g$) / cDelta) + 4);
|
||||||
|
|
||||||
|
//sat
|
||||||
|
if (l > 50)
|
||||||
|
s = 100 * (cDelta / (2 - maxAndMin));
|
||||||
|
else
|
||||||
|
s = 100 * (cDelta / maxAndMin);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [Math.floor(h), Math.floor(s), Math.floor(l)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace pxsim.visuals {
|
||||||
|
const PIXEL_SPACING = PIN_DIST * 3;
|
||||||
|
const PIXEL_RADIUS = PIN_DIST;
|
||||||
|
const CANVAS_WIDTH = 1.2 * PIN_DIST;
|
||||||
|
const CANVAS_HEIGHT = 12 * PIN_DIST;
|
||||||
|
const CANVAS_VIEW_WIDTH = CANVAS_WIDTH;
|
||||||
|
const CANVAS_VIEW_HEIGHT = CANVAS_HEIGHT;
|
||||||
|
const CANVAS_VIEW_PADDING = PIN_DIST * 4;
|
||||||
|
const CANVAS_LEFT = 1.4 * PIN_DIST;
|
||||||
|
const CANVAS_TOP = PIN_DIST;
|
||||||
|
|
||||||
|
// For the instructions parts list
|
||||||
|
export function mkNeoPixelPart(xy: Coord = [0, 0]): SVGElAndSize {
|
||||||
|
const NP_PART_XOFF = -13.5;
|
||||||
|
const NP_PART_YOFF = -11;
|
||||||
|
const NP_PART_WIDTH = 87.5;
|
||||||
|
const NP_PART_HEIGHT = 190;
|
||||||
|
const NEOPIXEL_PART_IMG = "neopixel.svg";
|
||||||
|
let [x, y] = xy;
|
||||||
|
let l = x + NP_PART_XOFF;
|
||||||
|
let t = y + NP_PART_YOFF;
|
||||||
|
let w = NP_PART_WIDTH;
|
||||||
|
let h = NP_PART_HEIGHT;
|
||||||
|
let img = <SVGImageElement>svg.elt("image");
|
||||||
|
svg.hydrate(img, {class: "sim-neopixel-strip", x: l, y: t, width: w, height: h,
|
||||||
|
href: `/static/hardware/${NEOPIXEL_PART_IMG}`});
|
||||||
|
return {el: img, x: l, y: t, w: w, h: h};
|
||||||
|
}
|
||||||
|
export class NeoPixel implements SVGAndSize<SVGCircleElement> {
|
||||||
|
public el: SVGCircleElement;
|
||||||
|
public w: number;
|
||||||
|
public h: number;
|
||||||
|
public x: number;
|
||||||
|
public y: number;
|
||||||
|
public cx: number;
|
||||||
|
public cy: number;
|
||||||
|
|
||||||
|
constructor(xy: Coord = [0, 0]) {
|
||||||
|
let circle = <SVGCircleElement>svg.elt("circle");
|
||||||
|
let r = PIXEL_RADIUS;
|
||||||
|
let [cx, cy] = xy;
|
||||||
|
svg.hydrate(circle, {cx: cx, cy: cy, r: r, class: "sim-neopixel"});
|
||||||
|
this.el = circle;
|
||||||
|
this.w = r * 2;
|
||||||
|
this.h = r * 2;
|
||||||
|
this.x = cx - r;
|
||||||
|
this.y = cy - r;
|
||||||
|
this.cx = cx;
|
||||||
|
this.cy = cy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setRgb(rgb: [number, number, number]) {
|
||||||
|
let hsl = rgbToHsl(rgb);
|
||||||
|
let [h, s, l] = hsl;
|
||||||
|
//We ignore luminosity since it doesn't map well to real-life brightness
|
||||||
|
let fill = `hsl(${h}, ${s}%, 70%)`;
|
||||||
|
this.el.setAttribute("fill", fill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NeoPixelCanvas {
|
||||||
|
public canvas: SVGSVGElement;
|
||||||
|
public pin: number;
|
||||||
|
public pixels: NeoPixel[];
|
||||||
|
private viewBox: [number, number, number, number];
|
||||||
|
private background: SVGRectElement;
|
||||||
|
|
||||||
|
constructor(pin: number) {
|
||||||
|
this.pixels = [];
|
||||||
|
this.pin = pin;
|
||||||
|
let el = <SVGSVGElement>svg.elt("svg");
|
||||||
|
svg.hydrate(el, {
|
||||||
|
"class": `sim-neopixel-canvas`,
|
||||||
|
"x": "0px",
|
||||||
|
"y": "0px",
|
||||||
|
"width": `${CANVAS_WIDTH}px`,
|
||||||
|
"height": `${CANVAS_HEIGHT}px`,
|
||||||
|
});
|
||||||
|
this.canvas = el;
|
||||||
|
this.background = <SVGRectElement>svg.child(el, "rect", { class: "sim-neopixel-background hidden"});
|
||||||
|
this.updateViewBox(-CANVAS_VIEW_WIDTH / 2, 0, CANVAS_VIEW_WIDTH, CANVAS_VIEW_HEIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateViewBox(x: number, y: number, w: number, h: number) {
|
||||||
|
this.viewBox = [x, y, w, h];
|
||||||
|
svg.hydrate(this.canvas, {"viewBox": `${x} ${y} ${w} ${h}`});
|
||||||
|
svg.hydrate(this.background, {"x": x, "y": y, "width": w, "height": h});
|
||||||
|
}
|
||||||
|
|
||||||
|
public update(colors: RGBW[]) {
|
||||||
|
if (!colors || colors.length <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (let i = 0; i < colors.length; i++) {
|
||||||
|
let pixel = this.pixels[i];
|
||||||
|
if (!pixel) {
|
||||||
|
let cxy: Coord = [0, CANVAS_VIEW_PADDING + i * PIXEL_SPACING];
|
||||||
|
pixel = this.pixels[i] = new NeoPixel(cxy);
|
||||||
|
this.canvas.appendChild(pixel.el);
|
||||||
|
}
|
||||||
|
let color = colors[i];
|
||||||
|
pixel.setRgb(color);
|
||||||
|
svg.hydrate(pixel.el, {title: `offset: ${i}`});
|
||||||
|
}
|
||||||
|
|
||||||
|
//show the canvas if it's hidden
|
||||||
|
svg.removeClass(this.background, "hidden");
|
||||||
|
|
||||||
|
//resize if necessary
|
||||||
|
let [first, last] = [this.pixels[0], this.pixels[this.pixels.length - 1]]
|
||||||
|
let yDiff = last.cy - first.cy;
|
||||||
|
let newH = yDiff + CANVAS_VIEW_PADDING * 2;
|
||||||
|
let [oldX, oldY, oldW, oldH] = this.viewBox;
|
||||||
|
if (oldH < newH) {
|
||||||
|
let scalar = newH / oldH;
|
||||||
|
let newW = oldW * scalar;
|
||||||
|
this.updateViewBox(-newW / 2, oldY, newW, newH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public setLoc(xy: Coord) {
|
||||||
|
let [x, y] = xy;
|
||||||
|
svg.hydrate(this.canvas, {x: x, y: y});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function gpioPinToPinNumber(gpioPin: string): number {
|
||||||
|
let pinNumStr = gpioPin.split("P")[1];
|
||||||
|
let pinNum = Number(pinNumStr) + 7 /*MICROBIT_ID_IO_P0; TODO: don't hardcode this, import enums.d.ts*/;
|
||||||
|
return pinNum
|
||||||
|
}
|
||||||
|
function parseNeoPixelMode(modeStr: string): NeoPixelMode {
|
||||||
|
const modeMap: Map<NeoPixelMode> = {
|
||||||
|
"NeoPixelMode.RGB": NeoPixelMode.RGB,
|
||||||
|
"NeoPixelMode.RGBW": NeoPixelMode.RGBW,
|
||||||
|
"*": NeoPixelMode.RGB,
|
||||||
|
};
|
||||||
|
let mode: NeoPixelMode = null;
|
||||||
|
for (let key in modeMap) {
|
||||||
|
if (key == modeStr) {
|
||||||
|
mode = modeMap[key];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
U.assert(mode != null, "Unknown NeoPixelMode: " + modeStr);
|
||||||
|
return mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NeoPixelView implements IBoardComponent<NeoPixelState> {
|
||||||
|
public style: string = `
|
||||||
|
.sim-neopixel-canvas {
|
||||||
|
}
|
||||||
|
.sim-neopixel-canvas-parent:hover {
|
||||||
|
transform-origin: center;
|
||||||
|
transform: scale(4) translateY(-60px);
|
||||||
|
}
|
||||||
|
.sim-neopixel-canvas .hidden {
|
||||||
|
visibility:hidden;
|
||||||
|
}
|
||||||
|
.sim-neopixel-background {
|
||||||
|
fill: rgba(255,255,255,0.9);
|
||||||
|
}
|
||||||
|
.sim-neopixel-strip {
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
public element: SVGElement;
|
||||||
|
public defs: SVGElement[];
|
||||||
|
private state: NeoPixelState;
|
||||||
|
private canvas: NeoPixelCanvas;
|
||||||
|
private part: SVGElAndSize;
|
||||||
|
private stripGroup: SVGGElement;
|
||||||
|
private lastLocation: Coord;
|
||||||
|
private pin: number;
|
||||||
|
private mode: NeoPixelMode;
|
||||||
|
|
||||||
|
public init(bus: EventBus, state: NeoPixelState, svgEl: SVGSVGElement, gpioPins: string[], otherArgs: string[]): void {
|
||||||
|
U.assert(otherArgs.length === 1, "NeoPixels assumes a RGB vs RGBW mode is passed to it");
|
||||||
|
let modeStr = otherArgs[0];
|
||||||
|
this.mode = parseNeoPixelMode(modeStr);
|
||||||
|
this.state = state;
|
||||||
|
this.stripGroup = <SVGGElement>svg.elt("g");
|
||||||
|
this.element = this.stripGroup;
|
||||||
|
let pinStr = gpioPins[0];
|
||||||
|
this.pin = gpioPinToPinNumber(pinStr);
|
||||||
|
this.lastLocation = [0, 0];
|
||||||
|
let part = mkNeoPixelPart();
|
||||||
|
this.part = part;
|
||||||
|
this.stripGroup.appendChild(part.el);
|
||||||
|
let canvas = new NeoPixelCanvas(this.pin);
|
||||||
|
this.canvas = canvas;
|
||||||
|
let canvasG = svg.child(this.stripGroup, "g", {class: "sim-neopixel-canvas-parent"});
|
||||||
|
canvasG.appendChild(canvas.canvas);
|
||||||
|
this.updateStripLoc();
|
||||||
|
}
|
||||||
|
public moveToCoord(xy: Coord): void {
|
||||||
|
let [x, y] = xy;
|
||||||
|
let loc: Coord = [x, y];
|
||||||
|
this.lastLocation = loc;
|
||||||
|
this.updateStripLoc();
|
||||||
|
}
|
||||||
|
private updateStripLoc() {
|
||||||
|
let [x, y] = this.lastLocation;
|
||||||
|
this.canvas.setLoc([x + CANVAS_LEFT, y + CANVAS_TOP]);
|
||||||
|
svg.hydrate(this.part.el, {transform: `translate(${x} ${y})`}); //TODO: update part's l,h, etc.
|
||||||
|
}
|
||||||
|
public updateState(): void {
|
||||||
|
let colors = this.state.getColors(this.pin, this.mode);
|
||||||
|
this.canvas.update(colors);
|
||||||
|
}
|
||||||
|
public updateTheme (): void { }
|
||||||
|
}
|
||||||
|
}
|
425
sim/visuals/wiring.ts
Normal file
425
sim/visuals/wiring.ts
Normal file
|
@ -0,0 +1,425 @@
|
||||||
|
namespace pxsim.visuals {
|
||||||
|
const WIRE_WIDTH = PIN_DIST / 2.5;
|
||||||
|
const BB_WIRE_SMOOTH = 0.7;
|
||||||
|
const INSTR_WIRE_SMOOTH = 0.8;
|
||||||
|
const WIRE_PART_CURVE_OFF = 15;
|
||||||
|
const WIRE_PART_LENGTH = 100;
|
||||||
|
export const WIRES_CSS = `
|
||||||
|
.sim-bb-wire {
|
||||||
|
fill:none;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-width:${WIRE_WIDTH}px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.sim-bb-wire-end {
|
||||||
|
stroke:#333;
|
||||||
|
fill:#333;
|
||||||
|
}
|
||||||
|
.sim-bb-wire-bare-end {
|
||||||
|
fill: #ccc;
|
||||||
|
}
|
||||||
|
.sim-bb-wire-hover {
|
||||||
|
stroke-width: ${WIRE_WIDTH}px;
|
||||||
|
visibility: hidden;
|
||||||
|
stroke-dasharray: ${PIN_DIST / 10.0},${PIN_DIST / 1.5};
|
||||||
|
/*stroke-opacity: 0.4;*/
|
||||||
|
}
|
||||||
|
.grayed .sim-bb-wire-ends-g:not(.highlight) .sim-bb-wire-end {
|
||||||
|
stroke: #777;
|
||||||
|
fill: #777;
|
||||||
|
}
|
||||||
|
.grayed .sim-bb-wire:not(.highlight) {
|
||||||
|
stroke: #CCC;
|
||||||
|
}
|
||||||
|
.sim-bb-wire-ends-g:hover .sim-bb-wire-end {
|
||||||
|
stroke: red;
|
||||||
|
fill: red;
|
||||||
|
}
|
||||||
|
.sim-bb-wire-ends-g:hover .sim-bb-wire-bare-end {
|
||||||
|
stroke: #FFF;
|
||||||
|
fill: #FFF;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export interface Wire {
|
||||||
|
endG: SVGGElement;
|
||||||
|
end1: SVGElement;
|
||||||
|
end2: SVGElement;
|
||||||
|
wires: SVGElement[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function cssEncodeColor(color: string): string {
|
||||||
|
//HACK/TODO: do real CSS encoding.
|
||||||
|
return color
|
||||||
|
.replace(/\#/g, "-")
|
||||||
|
.replace(/\(/g, "-")
|
||||||
|
.replace(/\)/g, "-")
|
||||||
|
.replace(/\,/g, "-")
|
||||||
|
.replace(/\./g, "-")
|
||||||
|
.replace(/\s/g, "");
|
||||||
|
}
|
||||||
|
export enum WireEndStyle {
|
||||||
|
BBJumper,
|
||||||
|
OpenJumper,
|
||||||
|
Croc,
|
||||||
|
}
|
||||||
|
export interface WireOpts { //TODO: use throughout
|
||||||
|
color?: string,
|
||||||
|
colorClass?: string,
|
||||||
|
bendFactor?: number,
|
||||||
|
}
|
||||||
|
export function mkWirePart(cp: [number, number], clr: string, croc: boolean = false): visuals.SVGAndSize<SVGGElement> {
|
||||||
|
let g = <SVGGElement>svg.elt("g");
|
||||||
|
let [cx, cy] = cp;
|
||||||
|
let offset = WIRE_PART_CURVE_OFF;
|
||||||
|
let p1: visuals.Coord = [cx - offset, cy - WIRE_PART_LENGTH / 2];
|
||||||
|
let p2: visuals.Coord = [cx + offset, cy + WIRE_PART_LENGTH / 2];
|
||||||
|
clr = visuals.mapWireColor(clr);
|
||||||
|
let e1: SVGElAndSize;
|
||||||
|
if (croc)
|
||||||
|
e1 = mkCrocEnd(p1, true, clr);
|
||||||
|
else
|
||||||
|
e1 = mkOpenJumperEnd(p1, true, clr);
|
||||||
|
let s = mkWirePartSeg(p1, p2, clr);
|
||||||
|
let e2 = mkOpenJumperEnd(p2, false, clr);
|
||||||
|
g.appendChild(s.el);
|
||||||
|
g.appendChild(e1.el);
|
||||||
|
g.appendChild(e2.el);
|
||||||
|
let l = Math.min(e1.x, e2.x);
|
||||||
|
let r = Math.max(e1.x + e1.w, e2.x + e2.w);
|
||||||
|
let t = Math.min(e1.y, e2.y);
|
||||||
|
let b = Math.max(e1.y + e1.h, e2.y + e2.h);
|
||||||
|
return {el: g, x: l, y: t, w: r - l, h: b - t};
|
||||||
|
}
|
||||||
|
function mkCurvedWireSeg(p1: [number, number], p2: [number, number], smooth: number, clrClass: string): SVGPathElement {
|
||||||
|
const coordStr = (xy: [number, number]): string => {return `${xy[0]}, ${xy[1]}`};
|
||||||
|
let [x1, y1] = p1;
|
||||||
|
let [x2, y2] = p2
|
||||||
|
let yLen = (y2 - y1);
|
||||||
|
let c1: [number, number] = [x1, y1 + yLen * smooth];
|
||||||
|
let c2: [number, number] = [x2, y2 - yLen * smooth];
|
||||||
|
let w = <SVGPathElement>svg.mkPath("sim-bb-wire", `M${coordStr(p1)} C${coordStr(c1)} ${coordStr(c2)} ${coordStr(p2)}`);
|
||||||
|
svg.addClass(w, `wire-stroke-${clrClass}`);
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
function mkWirePartSeg(p1: [number, number], p2: [number, number], clr: string): visuals.SVGAndSize<SVGPathElement> {
|
||||||
|
//TODO: merge with mkCurvedWireSeg
|
||||||
|
const coordStr = (xy: [number, number]): string => {return `${xy[0]}, ${xy[1]}`};
|
||||||
|
let [x1, y1] = p1;
|
||||||
|
let [x2, y2] = p2
|
||||||
|
let yLen = (y2 - y1);
|
||||||
|
let c1: [number, number] = [x1, y1 + yLen * .8];
|
||||||
|
let c2: [number, number] = [x2, y2 - yLen * .8];
|
||||||
|
let e = <SVGPathElement>svg.mkPath("sim-bb-wire", `M${coordStr(p1)} C${coordStr(c1)} ${coordStr(c2)} ${coordStr(p2)}`);
|
||||||
|
(<any>e).style["stroke"] = clr;
|
||||||
|
return {el: e, x: Math.min(x1, x2), y: Math.min(y1, y2), w: Math.abs(x1 - x2), h: Math.abs(y1 - y2)};
|
||||||
|
}
|
||||||
|
function mkWireSeg(p1: [number, number], p2: [number, number], clrClass: string): SVGPathElement {
|
||||||
|
const coordStr = (xy: [number, number]): string => {return `${xy[0]}, ${xy[1]}`};
|
||||||
|
let w = <SVGPathElement>svg.mkPath("sim-bb-wire", `M${coordStr(p1)} L${coordStr(p2)}`);
|
||||||
|
svg.addClass(w, `wire-stroke-${clrClass}`);
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
function mkBBJumperEnd(p: [number, number], clrClass: string): SVGElement {
|
||||||
|
const endW = PIN_DIST / 4;
|
||||||
|
let w = svg.elt("circle");
|
||||||
|
let x = p[0];
|
||||||
|
let y = p[1];
|
||||||
|
let r = WIRE_WIDTH / 2 + endW / 2;
|
||||||
|
svg.hydrate(w, {cx: x, cy: y, r: r, class: "sim-bb-wire-end"});
|
||||||
|
svg.addClass(w, `wire-fill-${clrClass}`);
|
||||||
|
(<any>w).style["stroke-width"] = `${endW}px`;
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
function mkOpenJumperEnd(p: [number, number], top: boolean, clr: string): visuals.SVGElAndSize {
|
||||||
|
let k = visuals.PIN_DIST * 0.24;
|
||||||
|
let plasticLength = k * 10;
|
||||||
|
let plasticWidth = k * 2;
|
||||||
|
let metalLength = k * 6;
|
||||||
|
let metalWidth = k;
|
||||||
|
const strokeWidth = visuals.PIN_DIST / 4.0;
|
||||||
|
let [cx, cy] = p;
|
||||||
|
let o = top ? -1 : 1;
|
||||||
|
let g = svg.elt("g")
|
||||||
|
|
||||||
|
let el = svg.elt("rect");
|
||||||
|
let h1 = plasticLength;
|
||||||
|
let w1 = plasticWidth;
|
||||||
|
let x1 = cx - w1 / 2;
|
||||||
|
let y1 = cy - (h1 / 2);
|
||||||
|
svg.hydrate(el, {x: x1, y: y1, width: w1, height: h1, rx: 0.5, ry: 0.5, class: "sim-bb-wire-end"});
|
||||||
|
(<any>el).style["stroke-width"] = `${strokeWidth}px`;
|
||||||
|
|
||||||
|
let el2 = svg.elt("rect");
|
||||||
|
let h2 = metalLength;
|
||||||
|
let w2 = metalWidth;
|
||||||
|
let cy2 = cy + o * (h1 / 2 + h2 / 2);
|
||||||
|
let x2 = cx - w2 / 2;
|
||||||
|
let y2 = cy2 - (h2 / 2);
|
||||||
|
svg.hydrate(el2, {x: x2, y: y2, width: w2, height: h2, class: "sim-bb-wire-bare-end"});
|
||||||
|
(<any>el2).style["fill"] = `#bbb`;
|
||||||
|
|
||||||
|
g.appendChild(el2);
|
||||||
|
g.appendChild(el);
|
||||||
|
return {el: g, x: x1 - strokeWidth, y: Math.min(y1, y2), w: w1 + strokeWidth * 2, h: h1 + h2};
|
||||||
|
}
|
||||||
|
function mkCrocEnd(p: [number, number], top: boolean, clr: string): SVGElAndSize {
|
||||||
|
//TODO: merge with mkOpenJumperEnd()
|
||||||
|
let k = visuals.PIN_DIST * 0.24;
|
||||||
|
const plasticWidth = k * 4;
|
||||||
|
const plasticLength = k * 10.0;
|
||||||
|
const metalWidth = k * 3.5;
|
||||||
|
const metalHeight = k * 3.5;
|
||||||
|
const pointScalar = .15;
|
||||||
|
const baseScalar = .3;
|
||||||
|
const taperScalar = .7;
|
||||||
|
const strokeWidth = visuals.PIN_DIST / 4.0;
|
||||||
|
let [cx, cy] = p;
|
||||||
|
let o = top ? -1 : 1;
|
||||||
|
let g = svg.elt("g")
|
||||||
|
|
||||||
|
let el = svg.elt("polygon");
|
||||||
|
let h1 = plasticLength;
|
||||||
|
let w1 = plasticWidth;
|
||||||
|
let x1 = cx - w1 / 2;
|
||||||
|
let y1 = cy - (h1 / 2);
|
||||||
|
let mkPnt = (xy: Coord) => `${xy[0]},${xy[1]}`;
|
||||||
|
let mkPnts = (...xys: Coord[]) => xys.map(xy => mkPnt(xy)).join(" ");
|
||||||
|
const topScalar = top ? pointScalar : baseScalar;
|
||||||
|
const midScalar = top ? taperScalar : (1 - taperScalar);
|
||||||
|
const botScalar = top ? baseScalar : pointScalar;
|
||||||
|
svg.hydrate(el, {
|
||||||
|
points: mkPnts(
|
||||||
|
[x1 + w1 * topScalar, y1], //TL
|
||||||
|
[x1 + w1 * (1 - topScalar), y1], //TR
|
||||||
|
[x1 + w1, y1 + h1 * midScalar], //MR
|
||||||
|
[x1 + w1 * (1 - botScalar), y1 + h1], //BR
|
||||||
|
[x1 + w1 * botScalar, y1 + h1], //BL
|
||||||
|
[x1, y1 + h1 * midScalar]) //ML
|
||||||
|
});
|
||||||
|
svg.hydrate(el, {rx: 0.5, ry: 0.5, class: "sim-bb-wire-end"});
|
||||||
|
(<any>el).style["stroke-width"] = `${strokeWidth}px`;
|
||||||
|
|
||||||
|
let el2 = svg.elt("rect");
|
||||||
|
let h2 = metalWidth;
|
||||||
|
let w2 = metalHeight;
|
||||||
|
let cy2 = cy + o * (h1 / 2 + h2 / 2);
|
||||||
|
let x2 = cx - w2 / 2;
|
||||||
|
let y2 = cy2 - (h2 / 2);
|
||||||
|
svg.hydrate(el2, {x: x2, y: y2, width: w2, height: h2, class: "sim-bb-wire-bare-end"});
|
||||||
|
|
||||||
|
g.appendChild(el2);
|
||||||
|
g.appendChild(el);
|
||||||
|
return {el: g, x: x1 - strokeWidth, y: Math.min(y1, y2), w: w1 + strokeWidth * 2, h: h1 + h2};
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: make this stupid class obsolete
|
||||||
|
export class WireFactory {
|
||||||
|
private underboard: SVGGElement;
|
||||||
|
private overboard: SVGGElement;
|
||||||
|
private boardEdges: number[];
|
||||||
|
private getLocCoord: (loc: Loc) => Coord;
|
||||||
|
public styleEl: SVGStyleElement;
|
||||||
|
|
||||||
|
constructor(underboard: SVGGElement, overboard: SVGGElement, boardEdges: number[], styleEl: SVGStyleElement, getLocCoord: (loc: Loc) => Coord) {
|
||||||
|
this.styleEl = styleEl;
|
||||||
|
this.styleEl.textContent += WIRES_CSS;
|
||||||
|
this.underboard = underboard;
|
||||||
|
this.overboard = overboard;
|
||||||
|
this.boardEdges = boardEdges;
|
||||||
|
this.getLocCoord = getLocCoord;
|
||||||
|
}
|
||||||
|
|
||||||
|
private indexOfMin(vs: number[]): number {
|
||||||
|
let minIdx = 0;
|
||||||
|
let min = vs[0];
|
||||||
|
for (let i = 1; i < vs.length; i++) {
|
||||||
|
if (vs[i] < min) {
|
||||||
|
min = vs[i];
|
||||||
|
minIdx = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return minIdx;
|
||||||
|
}
|
||||||
|
private closestEdgeIdx(p: [number, number]): number {
|
||||||
|
let dists = this.boardEdges.map(e => Math.abs(p[1] - e));
|
||||||
|
let edgeIdx = this.indexOfMin(dists);
|
||||||
|
return edgeIdx;
|
||||||
|
}
|
||||||
|
private closestEdge(p: [number, number]): number {
|
||||||
|
return this.boardEdges[this.closestEdgeIdx(p)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private nextWireId = 0;
|
||||||
|
private drawWire(pin1: Coord, pin2: Coord, color: string): Wire {
|
||||||
|
let wires: SVGElement[] = [];
|
||||||
|
let g = svg.child(this.overboard, "g", {class: "sim-bb-wire-group"});
|
||||||
|
const closestPointOffBoard = (p: [number, number]): [number, number] => {
|
||||||
|
const offset = PIN_DIST / 2;
|
||||||
|
let e = this.closestEdge(p);
|
||||||
|
let y: number;
|
||||||
|
if (e - p[1] < 0)
|
||||||
|
y = e - offset;
|
||||||
|
else
|
||||||
|
y = e + offset;
|
||||||
|
return [p[0], y];
|
||||||
|
}
|
||||||
|
let wireId = this.nextWireId++;
|
||||||
|
let clrClass = cssEncodeColor(color);
|
||||||
|
let end1 = mkBBJumperEnd(pin1, clrClass);
|
||||||
|
let end2 = mkBBJumperEnd(pin2, clrClass);
|
||||||
|
let endG = <SVGGElement>svg.child(g, "g", {class: "sim-bb-wire-ends-g"});
|
||||||
|
endG.appendChild(end1);
|
||||||
|
endG.appendChild(end2);
|
||||||
|
let edgeIdx1 = this.closestEdgeIdx(pin1);
|
||||||
|
let edgeIdx2 = this.closestEdgeIdx(pin2);
|
||||||
|
if (edgeIdx1 == edgeIdx2) {
|
||||||
|
let seg = mkWireSeg(pin1, pin2, clrClass);
|
||||||
|
g.appendChild(seg);
|
||||||
|
wires.push(seg);
|
||||||
|
} else {
|
||||||
|
let offP1 = closestPointOffBoard(pin1);
|
||||||
|
let offP2 = closestPointOffBoard(pin2);
|
||||||
|
let offSeg1 = mkWireSeg(pin1, offP1, clrClass);
|
||||||
|
let offSeg2 = mkWireSeg(pin2, offP2, clrClass);
|
||||||
|
let midSeg: SVGElement;
|
||||||
|
let midSegHover: SVGElement;
|
||||||
|
let isBetweenMiddleTwoEdges = (edgeIdx1 == 1 || edgeIdx1 == 2) && (edgeIdx2 == 1 || edgeIdx2 == 2);
|
||||||
|
if (isBetweenMiddleTwoEdges) {
|
||||||
|
midSeg = mkCurvedWireSeg(offP1, offP2, BB_WIRE_SMOOTH, clrClass);
|
||||||
|
midSegHover = mkCurvedWireSeg(offP1, offP2, BB_WIRE_SMOOTH, clrClass);
|
||||||
|
} else {
|
||||||
|
midSeg = mkWireSeg(offP1, offP2, clrClass);
|
||||||
|
midSegHover = mkWireSeg(offP1, offP2, clrClass);
|
||||||
|
}
|
||||||
|
svg.addClass(midSegHover, "sim-bb-wire-hover");
|
||||||
|
g.appendChild(offSeg1);
|
||||||
|
wires.push(offSeg1);
|
||||||
|
g.appendChild(offSeg2);
|
||||||
|
wires.push(offSeg2);
|
||||||
|
this.underboard.appendChild(midSeg);
|
||||||
|
wires.push(midSeg);
|
||||||
|
g.appendChild(midSegHover);
|
||||||
|
wires.push(midSegHover);
|
||||||
|
//set hover mechanism
|
||||||
|
let wireIdClass = `sim-bb-wire-id-${wireId}`;
|
||||||
|
const setId = (e: SVGElement) => svg.addClass(e, wireIdClass);
|
||||||
|
setId(endG);
|
||||||
|
setId(midSegHover);
|
||||||
|
this.styleEl.textContent += `
|
||||||
|
.${wireIdClass}:hover ~ .${wireIdClass}.sim-bb-wire-hover {
|
||||||
|
visibility: visible;
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// wire colors
|
||||||
|
let colorCSS = `
|
||||||
|
.wire-stroke-${clrClass} {
|
||||||
|
stroke: ${mapWireColor(color)};
|
||||||
|
}
|
||||||
|
.wire-fill-${clrClass} {
|
||||||
|
fill: ${mapWireColor(color)};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
this.styleEl.textContent += colorCSS;
|
||||||
|
|
||||||
|
return {endG: endG, end1: end1, end2: end2, wires: wires};
|
||||||
|
}
|
||||||
|
private drawWireWithCrocs(pin1: Coord, pin2: Coord, color: string): Wire {
|
||||||
|
//TODO: merge with drawWire()
|
||||||
|
const PIN_Y_OFF = 40;
|
||||||
|
const CROC_Y_OFF = -17;
|
||||||
|
let wires: SVGElement[] = [];
|
||||||
|
let g = svg.child(this.overboard, "g", {class: "sim-bb-wire-group"});
|
||||||
|
const closestPointOffBoard = (p: [number, number]): [number, number] => {
|
||||||
|
const offset = PIN_DIST / 2;
|
||||||
|
let e = this.closestEdge(p);
|
||||||
|
let y: number;
|
||||||
|
if (e - p[1] < 0)
|
||||||
|
y = e - offset;
|
||||||
|
else
|
||||||
|
y = e + offset;
|
||||||
|
return [p[0], y];
|
||||||
|
}
|
||||||
|
let wireId = this.nextWireId++;
|
||||||
|
let clrClass = cssEncodeColor(color);
|
||||||
|
let end1 = mkBBJumperEnd(pin1, clrClass);
|
||||||
|
let pin2orig = pin2;
|
||||||
|
let [x2, y2] = pin2;
|
||||||
|
pin2 = [x2, y2 + PIN_Y_OFF];//HACK
|
||||||
|
[x2, y2] = pin2;
|
||||||
|
let endCoord2: Coord = [x2, y2 + CROC_Y_OFF]
|
||||||
|
let end2AndSize = mkCrocEnd(endCoord2, true, color);
|
||||||
|
let end2 = end2AndSize.el;
|
||||||
|
let endG = <SVGGElement>svg.child(g, "g", {class: "sim-bb-wire-ends-g"});
|
||||||
|
endG.appendChild(end1);
|
||||||
|
//endG.appendChild(end2);
|
||||||
|
let edgeIdx1 = this.closestEdgeIdx(pin1);
|
||||||
|
let edgeIdx2 = this.closestEdgeIdx(pin2orig);
|
||||||
|
if (edgeIdx1 == edgeIdx2) {
|
||||||
|
let seg = mkWireSeg(pin1, pin2, clrClass);
|
||||||
|
g.appendChild(seg);
|
||||||
|
wires.push(seg);
|
||||||
|
} else {
|
||||||
|
let offP1 = closestPointOffBoard(pin1);
|
||||||
|
//let offP2 = closestPointOffBoard(pin2orig);
|
||||||
|
let offSeg1 = mkWireSeg(pin1, offP1, clrClass);
|
||||||
|
//let offSeg2 = mkWireSeg(pin2, offP2, clrClass);
|
||||||
|
let midSeg: SVGElement;
|
||||||
|
let midSegHover: SVGElement;
|
||||||
|
let isBetweenMiddleTwoEdges = (edgeIdx1 == 1 || edgeIdx1 == 2) && (edgeIdx2 == 1 || edgeIdx2 == 2);
|
||||||
|
if (isBetweenMiddleTwoEdges) {
|
||||||
|
midSeg = mkCurvedWireSeg(offP1, pin2, BB_WIRE_SMOOTH, clrClass);
|
||||||
|
midSegHover = mkCurvedWireSeg(offP1, pin2, BB_WIRE_SMOOTH, clrClass);
|
||||||
|
} else {
|
||||||
|
midSeg = mkWireSeg(offP1, pin2, clrClass);
|
||||||
|
midSegHover = mkWireSeg(offP1, pin2, clrClass);
|
||||||
|
}
|
||||||
|
svg.addClass(midSegHover, "sim-bb-wire-hover");
|
||||||
|
g.appendChild(offSeg1);
|
||||||
|
wires.push(offSeg1);
|
||||||
|
// g.appendChild(offSeg2);
|
||||||
|
// wires.push(offSeg2);
|
||||||
|
this.underboard.appendChild(midSeg);
|
||||||
|
wires.push(midSeg);
|
||||||
|
//g.appendChild(midSegHover);
|
||||||
|
//wires.push(midSegHover);
|
||||||
|
//set hover mechanism
|
||||||
|
let wireIdClass = `sim-bb-wire-id-${wireId}`;
|
||||||
|
const setId = (e: SVGElement) => svg.addClass(e, wireIdClass);
|
||||||
|
setId(endG);
|
||||||
|
setId(midSegHover);
|
||||||
|
this.styleEl.textContent += `
|
||||||
|
.${wireIdClass}:hover ~ .${wireIdClass}.sim-bb-wire-hover {
|
||||||
|
visibility: visible;
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
endG.appendChild(end2);//HACK
|
||||||
|
|
||||||
|
// wire colors
|
||||||
|
let colorCSS = `
|
||||||
|
.wire-stroke-${clrClass} {
|
||||||
|
stroke: ${mapWireColor(color)};
|
||||||
|
}
|
||||||
|
.wire-fill-${clrClass} {
|
||||||
|
fill: ${mapWireColor(color)};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
this.styleEl.textContent += colorCSS;
|
||||||
|
|
||||||
|
return {endG: endG, end1: end1, end2: end2, wires: wires};
|
||||||
|
}
|
||||||
|
|
||||||
|
public addWire(start: Loc, end: Loc, color: string, withCrocs: boolean = false): Wire {
|
||||||
|
let startLoc = this.getLocCoord(start);
|
||||||
|
let endLoc = this.getLocCoord(end);
|
||||||
|
let wireEls: Wire;
|
||||||
|
if (withCrocs && end.type == "dalboard") {
|
||||||
|
wireEls = this.drawWireWithCrocs(startLoc, endLoc, color);
|
||||||
|
} else {
|
||||||
|
wireEls = this.drawWire(startLoc, endLoc, color);
|
||||||
|
}
|
||||||
|
return wireEls;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user