443 lines
20 KiB
TypeScript
443 lines
20 KiB
TypeScript
|
|
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 if (location === "MOSI" || location === "MISO" || location === "SCK") {
|
|
if (!this.opts.boardDef.spiPins)
|
|
console.debug("No SPI pin mappings found!");
|
|
let pin = (<any>this.opts.boardDef.spiPins)[location as string] as string;
|
|
return {type: "dalboard", pin: pin};
|
|
} else if (location === "SDA" || location === "SCL") {
|
|
if (!this.opts.boardDef.i2cPins)
|
|
console.debug("No I2C pin mappings found!");
|
|
let pin = (<any>this.opts.boardDef.i2cPins)[location as string] as string;
|
|
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
|
|
debugger;
|
|
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();
|
|
}
|
|
} |