seveibar/f1c1990s-dev-board

This code manages the entire PCB assembly process for a computer hardware module, including component placement, schematic integration, automated routing, copper pouring, and design rule checks, focused on a four-layer carrier board with integrated support components and connectors.

Version
1.8.0
License
unset
Stars
0

scripts/verify.mjs

import fs from 'node:fs'
import assert from 'node:assert/strict'
const j=JSON.parse(fs.readFileSync('dist/index/circuit.json','utf8'))
const of=t=>j.filter(x=>x.type===t)
const parent=new Map()
const root=x=>{ if(!parent.has(x))parent.set(x,x); if(parent.get(x)!==x)parent.set(x,root(parent.get(x))); return parent.get(x) }
const join=(a,b)=>parent.set(root(a),root(b))
for(const t of of('source_trace')) {
  const ids=[...(t.connected_source_port_ids||[]),...(t.connected_source_net_ids||[])]
  ids.slice(1).forEach(id=>join(ids[0],id))
}
const comp=name=>{const c=of('source_component').filter(c=>c.name===name);assert.equal(c.length,1,`unique component ${name}`);return c[0]}
const port=(name,pin)=>{const c=comp(name);const p=of('source_port').find(p=>p.source_component_id===c.source_component_id && (p.pin_number===pin||p.name===pin));assert.ok(p,`${name}.${pin}`);return p.source_port_id}
const net=name=>{const n=of('source_net').find(n=>n.name===name);assert.ok(n,`net ${name}`);return n.source_net_id}
let checks=0
const expect=(name,pin,n)=>{assert.equal(root(port(name,pin)),root(net(n)),`${name}.${pin} -> ${n}`);checks++}
const rails=['BOARD_GND','V5_IN','V3V3','V1V8','V1V2']
assert.equal(new Set(rails.map(n=>root(net(n)))).size,5,'power domains are separate')
for(const [part,nets] of Object.entries({
 J_USB:['BOARD_GND','BOARD_GND','BOARD_GND','BOARD_GND',null,'USB_CC1','USB_N','USB_P','USB_N','USB_P',null,'USB_CC2','BOARD_GND','BOARD_GND','V5_IN','V5_IN'],
 J_SPI:['BOARD_GND','V3V3','SPI1_CLK','SPI1_MOSI','SPI1_MISO','SPI1_CS'],
 J_I2C:['BOARD_GND','V3V3','I2C_SDA','I2C_SCL'],
 U_FLASH:['FLASH_CS','FLASH_MISO','FLASH_WP','BOARD_GND','FLASH_MOSI','FLASH_CLK','FLASH_HOLD','V3V3'],
 D_RGB:['BOARD_GND','V5_IN',null,null,'LED_DATA','LED_CLK'],
 U_LED_BUF:['BOARD_GND','LED_DATA_3V3','LED_CLK_5V','BOARD_GND','LED_CLK_3V3','LED_DATA_5V','BOARD_GND','V5_IN'],
 JP_BOOT:['SOC_FLASH_CS','FLASH_CS']
})) nets.forEach((n,i)=>{if(n)expect(part,i+1,n)})
for(const [pin,n] of Object.entries({39:'SPI1_MISO',40:'SPI1_CLK',41:'SPI1_MOSI',42:'SPI1_CS',37:'I2C_SDA',38:'I2C_SCL',49:'UART_RX',48:'UART_TX',47:'LED_DATA_3V3',46:'LED_CLK_3V3',68:'USB_N',69:'USB_P',70:'RESET_N'}))expect('U1',+pin,n)
for(const pin of [5,20,50,67,80,4,73])expect('U1',pin,'V3V3')
for(const pin of [30,31,32,34,36])expect('U1',pin,'V1V8')
for(const pin of [22,35,71])expect('U1',pin,'V1V2')
assert.notEqual(root(net('SOC_FLASH_CS')),root(net('FLASH_CS')),'removable boot shunt remains separate in PCB netlist')
for(const [n,src,dst] of [['R_LED_DATA','LED_DATA_5V','LED_DATA'],['R_LED_CLK','LED_CLK_5V','LED_CLK'],['R_LED_DATA_PD','LED_DATA_3V3','BOARD_GND'],['R_LED_CLK_PD','LED_CLK_3V3','BOARD_GND']]){expect(n,1,src);expect(n,2,dst);}
assert.equal(comp('D_RGB').manufacturer_part_number,'SK9822-A');
assert.equal(comp('U_LED_BUF').manufacturer_part_number,'74AHCT2G125DC,125');
for(const pin of [3,4])assert.ok(!of('source_trace').some(t=>t.connected_source_port_ids.includes(port('D_RGB',pin))),'LED cascade outputs unconnected');
assert.ok(!of('source_component').some(c=>/^Q_|^R_B_|^R_PD_/.test(c.name)),'old transistor drivers removed');
expect('U_USB_ESD',2,'BOARD_GND');expect('U_USB_ESD',4,'USB_P');expect('U_USB_ESD',5,'V5_IN');expect('U_USB_ESD',6,'USB_N')
for(const [n,out] of [['U_1V2','V1V2'],['U_1V8','V1V8']]) {
 expect(n,1,'V3V3');expect(n,2,'BOARD_GND');expect(n,3,'V3V3');expect(n,5,out);
 assert.ok(!of('source_trace').some(t=>t.connected_source_port_ids.includes(port(n,4))),`${n} NC pin stays unconnected`);
}
for(const c of of('source_component').filter(c=>c.ftype!=='simple_test_point'))assert.ok(c.supplier_part_numbers?.jlcpcb?.some(id=>/^C\d+$/.test(id)),`${c.name} has JLCPCB part`);
for(const [n,netName] of [['TP_UART_TX','UART_TX'],['TP_UART_RX','UART_RX'],['TP_UART_GND','BOARD_GND']])expect(n,1,netName);
for(const [n,cc] of [['R_CC1','USB_CC1'],['R_CC2','USB_CC2']]) {
 expect(n,1,cc);expect(n,2,'BOARD_GND');assert.equal(comp(n).resistance,5100);
}
assert.notEqual(root(net('USB_CC1')),root(net('USB_CC2')),'CC pins have independent pull-downs');
for(const pin of [5,11])assert.ok(!of('source_trace').some(t=>t.connected_source_port_ids.includes(port('J_USB',pin))),'SBU is unconnected');
const usbPcb=of('pcb_component').find(p=>p.source_component_id===comp('J_USB').source_component_id);
assert.equal(usbPcb.rotation,180,'USB mouth faces top board edge');
assert.ok(!of('source_component').some(c=>c.name==='J_PROG'),'old programming JST removed');
const board=of('pcb_board')[0];assert.equal(board.width,58);assert.equal(board.height,48)
const errors=j.filter(x=>x.type.endsWith('_error'))
const decouplers=of('source_component').filter(c=>/^C_D\d+$/.test(c.name));
assert.equal(decouplers.length,15,'one dedicated decoupler per processor supply pin');
for(const c of decouplers){
 const cp=port(c.name,1),target=port('U1',Number(c.name.slice(3)));
 const traces=of('source_trace').filter(t=>t.connected_source_port_ids.includes(cp));
 assert.equal(traces.length,1,`${c.name} supply has one explicit trace`);
 assert.deepEqual([...traces[0].connected_source_port_ids].sort(),[cp,target].sort(),`${c.name} connects directly to its assigned U1 pin`);
 assert.deepEqual(traces[0].connected_source_net_ids,[],`${c.name} supply does not attach directly to a generic net`);
 assert.equal(root(port(c.name,2)),root(net('BOARD_GND')),`${c.name} return reaches ground`);
}
const carrierTargets = Object.fromEntries([
 ...['U_3V3','U_1V8','U_1V2'].flatMap(n=>[[`C_${n}_IN`,[n,n==='U_3V3'?8:1]],[`C_${n}_OUT`,[n,n==='U_3V3'?1:5]]]),
 ['C_FLASH',['U_FLASH',8]],['C_ESD',['U_USB_ESD',5]],['C_RGB',['D_RGB',2]],['C_LED_BUF',['U_LED_BUF',8]],
]);
const pcbPort=(n,p)=>of('pcb_port').find(e=>e.source_port_id===port(n,p));
const distance=(a,b)=>Math.hypot(a.x-b.x,a.y-b.y);
for(const [a,b] of [[7,9],[8,10]]) {
 const pa=pcbPort('J_USB',a),pb=pcbPort('J_USB',b);
 const t=of('pcb_trace').find(t=>t.route[0].start_pcb_port_id===pa.pcb_port_id&&t.route.at(-1).end_pcb_port_id===pb.pcb_port_id);
 assert.ok(t,'USB reversible contacts have a direct copper branch');
 assert.ok(t.route.every(p=>p.route_type==='wire'&&p.layer==='top'),'USB local joins have no vias');
 assert.ok(t.route.slice(1).reduce((len,p,i)=>len+distance(p,t.route[i]),0)<4,'USB local joins are shorter than 4 mm');
}

for(const [name,[target,pin]] of Object.entries(carrierTargets)) {
 const cp=port(name,1), tp=port(target,pin);
 assert.ok(of('source_trace').some(t=>t.connected_source_port_ids.length===2&&t.connected_source_port_ids.includes(cp)&&t.connected_source_port_ids.includes(tp)),`${name} targets ${target}.${pin}`);
 assert.ok(distance(pcbPort(name,1),pcbPort(target,pin))<2.5,`${name} supply pad within 2.5 mm`);
 assert.ok(distance(pcbPort(name,1),pcbPort(target,pin))<distance(pcbPort(name,2),pcbPort(target,pin)),`${name} faces target`);
}
let bulkAssertions=0;
for(const c of of('source_component').filter(c=>c.name.startsWith('C_B_') || ['C_TV_VRN','C_TV_VRP','C_TV_REF'].includes(c.name) || /^C_U_.*_(IN|OUT)$/.test(c.name))){
 const pcb=of('pcb_component').find(p=>p.source_component_id===c.source_component_id);
 const carrier=/^C_U_/.test(c.name);
 assert.ok(Math.max(pcb.width,pcb.height)>=2.8,`${c.name}: 0805 land pattern`);
 assert.equal(pcb.layer,'top',`${c.name}: correct assembly side`);
 assert.equal(c.max_voltage_rating,carrier?16:25,`${c.name}: capacitor voltage rating`);
 assert.equal(c.manufacturer_part_number,carrier?'GRM21BR71C475KE51L':c.name==='C_B_VCC_DRAM'?'GRM219R71E105KA88D':'GRM21BR61E106KA73L',`${c.name}: selected part`);
 bulkAssertions+=4;
}
assert.equal(bulkAssertions,64,'all sixteen larger capacitors checked');
const breakouts=of("source_component").filter(c=>c.ftype==="simple_test_point"&&!c.name.startsWith("TP_UART_"));
assert.equal(breakouts.length,25,"only connected module terminals are broken out");
const assembled=of('pcb_component').filter(p=>of('source_component').some(c=>c.source_component_id===p.source_component_id&&c.ftype!=='simple_test_point'));
assert.equal(assembled.length,81,'all physical assembly parts retained');
assert.ok(assembled.every(p=>p.layer==='top'),'single-sided assembly: all components on top');
assert.equal(of('pcb_smtpad').filter(p=>p.layer==='bottom').length,0,'no bottom-side SMT pads');
assert.ok(of('pcb_solder_paste').length>0,'top SMT paste retained');
assert.ok(of('pcb_solder_paste').every(p=>p.layer==='top'&&p.pcb_smtpad_id),'paste belongs only to top SMT pads');
const report={assemblySide:'top',assembledComponents:assembled.length,bottomAssemblyComponents:0,bulkCapacitorAssertions:bulkAssertions,carrierPlacementAssertions:Object.keys(carrierTargets).length*3,dedicatedDecouplers:decouplers.length,decouplingAssertions:decouplers.length*4+1,selectedModuleBreakouts:breakouts.length,electricalAssertions:checks+2,pcbComponents:of('pcb_component').length,pcbTraces:of('pcb_trace').length,pcbVias:of('pcb_via').length,errorsByType:errors.reduce((a,e)=>(a[e.type]=(a[e.type]||0)+1,a),{}),errors:errors.map(e=>({type:e.type,message:e.message}))}
fs.writeFileSync('verification.json',JSON.stringify(report,null,2)+'\n')
console.log(JSON.stringify({...report,errors:undefined},null,2))
if(errors.length)process.exitCode=1