astra/pd-power-supply

USB-PD Power Supply with RP2040 controller

Version
1.1.6
License
unset
Stars
0

scripts/verify-netlist.cjs

const fs=require('node:fs'),assert=require('node:assert/strict');
const d=JSON.parse(fs.readFileSync('dist/index/circuit.json','utf8'));
const source=name=>d.find(x=>x.type==='source_component'&&x.name===name);
const net=name=>d.find(x=>x.type==='source_net'&&x.name===name)?.subcircuit_connectivity_map_key;
function pin(name,label){const c=source(name);return d.find(x=>x.type==='source_port'&&x.source_component_id===c?.source_component_id&&x.port_hints.includes(label))?.subcircuit_connectivity_map_key}
function on(c,p,n){assert.ok(pin(c,p),`${c}.${p} missing`);assert.equal(pin(c,p),net(n),`${c}.${p} must connect to ${n}`)}
for(const c of ['J_LEVER']){on(c,'pin1','VOUT');on(c,'pin2','GND')}
on('F1','pin1','PD_RAW');on('F1','pin2','PD_BUS');
on('J_SWITCH','pin1','GND');on('J_SWITCH','pin2','OUTPUT_EN');on('J_SWITCH','pin3','V3V3');
assert.equal(source('J_SWITCH').manufacturer_part_number,'JS102011SAQN');
on('U_LOAD','IN','PD_BUS');on('U_LOAD','OUT','OUTPUT_BUS');on('U_LOAD','pin1','OUTPUT_EN');on('U_LOAD','pin2','GND');on('U_LOAD','GND','GND');
on('U_LOAD','ILM','LOAD_ILM');on('R_LOAD_ILM','pin2','GND');on('U_LOAD','DVDT','LOAD_DVDT');on('C_LOAD_DVDT','pin2','GND');
on('R_LOAD_EN','pin1','OUTPUT_EN');on('R_LOAD_EN','pin2','GND');
const switchPcb= d.find(x=>x.type==='pcb_component'&&x.source_component_id===source('J_SWITCH').source_component_id);
assert.equal(d.filter(x=>x.type==='pcb_smtpad'&&x.pcb_component_id===switchPcb.pcb_component_id).length,3);
assert.equal(d.filter(x=>x.type==='pcb_plated_hole'&&x.pcb_component_id===switchPcb.pcb_component_id).length,0);
assert.notEqual(net('PD_BUS'),net('OUTPUT_BUS'));
on('R_SHUNT','pin1','OUTPUT_BUS');on('R_SHUNT','pin2','VOUT');
on('U_SENSE','VIN_POS','OUTPUT_BUS');on('U_SENSE','VIN_NEG','VOUT');
for(const p of ['VBUS1','VBUS2']){on('J_DATA',p,'USB5');on('J_PWR',p,'PD_RAW')}
for(const [c,prefix] of [['J_PWR','PD'],['J_DATA','DATA']]){
 assert.equal(source(c).ftype,'simple_connector');assert.equal(source(c).standard,'usb_c');
 for(const p of ['GND1','GND2','SHELL1','SHELL2','SHELL3','SHELL4'])on(c,p,'GND');
 for(const p of ['CC1','CC2'])on(c,p,`${prefix}_${p}`);
 for(const p of ['DP1','DP2'])on(c,p,c==='J_PWR'?'PD_DP':'USB_DP');
 for(const p of ['DM1','DM2'])on(c,p,c==='J_PWR'?'PD_DM':'USB_DM');
}

assert.notEqual(net('USB5'),net('PD_RAW'));
on('D_USB','anode','USB5');on('D_USB','cathode','LOGIC_IN');
on('D_AUX','anode','PD_BUS');on('D_AUX','cathode','LOGIC_IN');
assert.equal(source('U_PD').manufacturer_part_number,'CH224A');
for(const p of ['VHV','VBUS'])on('U_PD',p,'PD_RAW');
on('U_PD','GND','GND');on('U_PD','CFG1','PD_CFG');
on('R_PD_CFG','pin2','GND');
for(const [p,n] of [['SDA','PD_SDA'],['SCL','PD_SCL'],['PG','PD_PG']])on('U_PD',p,n);
for(const [p,n] of [['GPIO2','PD_SDA'],['GPIO3','PD_SCL'],['GPIO4','PD_PG'],['GPIO26_ADC0','KNOB_ADC'],['GPIO27_ADC1','FINE_ADC']])on('U_MCU',p,n);
for(const [c,wiper,adc,r] of [['RV_COARSE','KNOB_WIPER','KNOB_ADC','R_KNOB'],['RV_FINE','FINE_WIPER','FINE_ADC','R_FINE']]){
 on(c,'pin1','GND');on(c,'pin2',wiper);on(c,'pin3','V3V3');on(r,'pin1',wiper);on(r,'pin2',adc);
}
assert.ok(!source('DS1'),'Removed display pin header');
for(const [p,n] of [['pin1','GND'],['pin2','V3V3'],['pin3','SCL'],['pin4','SDA']])on('J_I2C',p,n);
assert.equal(source('J_I2C').manufacturer_part_number,'B4B-PH-K-S(LF)(SN)');
for(const p of ['VIN','PVIN','RUN'])on('U_LOGIC',p,'LOGIC_IN');
for(const p of ['PGND1','PGND2','PGND3','PGND4','GND1','GND2','EPAD'])on('U_LOGIC',p,'GND');
on('U_LOGIC','PVOUT','V3V3');on('U_LOGIC','VCC','LOGIC_VCC');on('U_LOGIC','PVCC','LOGIC_VCC');
on('L_LOGIC','pin1','LOGIC_SW1');on('L_LOGIC','pin2','LOGIC_SW2');
on('R_FB_TOP','pin1','V3V3');on('R_FB_TOP','pin2','LOGIC_FB');on('R_FB_BOT','pin2','GND');
on('R_COMP','pin1','LOGIC_VC');on('R_COMP','pin2','COMP_C');
on('C_COMP','pin1','COMP_C');on('C_COMP','pin2','LOGIC_FB');
on('R_FF','pin1','V3V3');on('R_FF','pin2','FF_C');
on('C_FF','pin1','FF_C');on('C_FF','pin2','LOGIC_FB');
function pcb(name){return d.find(x=>x.type==='pcb_component'&&x.source_component_id===source(name)?.source_component_id)}
for(const c of ['J_DATA','J_PWR']){assert.ok(pcb(c).center.x< -34);assert.equal(pcb(c).rotation,270)}
assert.ok(!source('J_SCREW'),'Screw terminal removed');
assert.ok(pcb('J_LEVER').center.y< -12);assert.equal(pcb('J_LEVER').rotation,0);
const board=d.find(x=>x.type==='pcb_board');assert.equal(board.width,80);assert.equal(board.height,45);
assert.ok(!source('J_HEADER'),'No hidden parallel output header');
for(const c of ['RV_COARSE','RV_FINE']){assert.ok(pcb(c).center.y< -12);assert.equal(pcb(c).insertion_direction,'from_above')}
const holes=d.filter(x=>x.type==='pcb_hole'&&(x.hole_diameter===3.2||x.diameter===3.2));
assert.equal(holes.length,4);assert.deepEqual(holes.map(x=>[x.x,x.y]).sort(),[[-36,-18],[-29.5,18],[36,-18],[36,18]].sort());
for(const n of ['OLED','M3_BOSS1','M3_BOSS2','M3_BOSS3','M3_BOSS4','COARSE_GRIP','FINE_GRIP','J_SWITCH'])assert.ok(d.find(x=>x.type==='cad_component'&&x.source_component_id===source(n)?.source_component_id),`CAD missing: ${n}`);
assert.equal(d.filter(x=>x.type==='cad_component'&&x.source_component_id===source('CASE').source_component_id).length,2);
assert.equal(d.filter(x=>x.type.includes('error')).length,0,'Circuit errors remain');
console.log('PASS: PPS pins, dedicated I2C, both ADC dials, JST wiring, logic buck-boost, power topology, four M3 holes and enclosure CAD.');

const bare=JSON.parse(fs.readFileSync('dist/board/circuit.json','utf8'));
const bareNames=bare.filter(x=>x.type==='source_component').map(x=>x.name);
for(const n of ['CASE','OLED','COARSE_GRIP','FINE_GRIP','M3_BOSS1','M3_BOSS2','M3_BOSS3','M3_BOSS4'])assert.ok(!bareNames.includes(n),`Board-only view contains ${n}`);
assert.ok(bareNames.includes('J_SWITCH'));
assert.ok(!bareNames.includes('POWER_TOGGLE'));
assert.ok(!source('POWER_TOGGLE'));
assert.ok(bare.find(x=>x.type==='cad_component'&&x.source_component_id===bare.find(s=>s.type==='source_component'&&s.name==='J_SWITCH').source_component_id));
assert.equal(bare.filter(x=>x.type.includes('error')).length,0);
const placements=data=>{const names=Object.fromEntries(data.filter(x=>x.type==='source_component').map(x=>[x.source_component_id,x.name]));return data.filter(x=>x.type==='pcb_component'&&names[x.source_component_id]!=='CASE').map(x=>[names[x.source_component_id],x.center,x.rotation]).sort((a,b)=>a[0].localeCompare(b[0]))};
assert.deepEqual(placements(bare),placements(d),'Board and assembly must share placement');
console.log('PASS: maintained side slide switch and board-only view share an identical PCB with no assembly extras.');

assert.equal(d.filter(x=>x.type==='pcb_copper_pour' && ['inner1','inner2'].includes(x.layer)).length,2,'Two ground pours required');
console.log('PASS: 80 x 45 board, USB on narrow side, front terminals, maintained side SMD slide switch, inner ground pours.');

// Validate the SMD toggle interface against the emitted native enclosure solids.
const jscad=require('@jscad/modeling');
const {executeJscadOperations}=require('jscad-planner');
const cadFor=n=>d.filter(x=>x.type==='cad_component'&&x.source_component_id===source(n)?.source_component_id);
const solid=c=>jscad.transforms.translate([c.position.x,c.position.y,c.position.z],jscad.transforms.rotate([c.rotation.x,c.rotation.y,c.rotation.z].map(x=>x*Math.PI/180),executeJscadOperations(jscad,c.model_jscad)));
const switchCad=cadFor('J_SWITCH')[0];
assert.equal(switchPcb.rotation,270);assert.equal(switchPcb.insertion_direction,'from_left');
assert.ok(switchCad.model_obj_url.includes('C221660.obj'),'Use the programmer supplier model');
const vertices=[],polygons=[];
for(const line of fs.readFileSync('imports/JS102011SAQN.obj','utf8').split('\n')){
 const words=line.trim().split(/\s+/);
 if(words[0]==='v')vertices.push(words.slice(1,4).map(Number));
 if(words[0]==='f')polygons.push(jscad.geometries.poly3.fromPoints(words.slice(1).map(w=>vertices[Number(w.split('/')[0])-1])));
}
const origin=switchCad.model_origin_position;
const mesh=jscad.transforms.translate([-origin.x,-origin.y,-origin.z],jscad.geometries.geom3.create(polygons));
const toggle=jscad.transforms.translate([switchCad.position.x,switchCad.position.y,switchCad.position.z],jscad.transforms.rotate([switchCad.rotation.x,switchCad.rotation.y,switchCad.rotation.z].map(x=>x*Math.PI/180),mesh));
for(const shell of cadFor('CASE'))for(const position of [toggle,jscad.transforms.translate([0,2*switchCad.position.y,0],jscad.transforms.mirrorY(toggle))])assert.ok(jscad.measurements.measureVolume(jscad.booleans.intersect(position,solid(shell)))<0.01,'Both slide positions must clear both enclosure shells');
const bounds=jscad.measurements.measureBoundingBox(toggle);
assert.ok(bounds[0][2]>=-board.thickness/2-1e-6,'Locating pegs must clear the PCB underside');
assert.ok(bounds[0][0]<-43,'Toggle lever must project beyond left narrow wall');
assert.ok(switchPcb.center.x< -35 && switchPcb.center.y>15,'Switch is at the upper left');
assert.ok(!source('POWER_ROCKER'),'Old rocker must be removed');
console.log('PASS: side toggle projects outward, clears PCB height, and fits both native enclosure shells.');