MustafaMulla29/rp2040-bldc-motor-controller-new

A USB‑C PD–powered RP2040 BLDC motor controller PCB with 3.3 V logic, MCT8329A sensorless commutation, three MOSFET half-bridges, protected power switching, USB data, controls, and motor connectors.

Version
0.3.82
License
unset
Stars
0

scripts/check-native-stencil.mjs

/** Read the unmodified native Gerbers; never rewrite manufacturing geometry. */
import {readFileSync,writeFileSync} from 'node:fs'
import {resolve} from 'node:path'
import {createHash} from 'node:crypto'
import {unzipSync} from 'fflate'
import assert from 'node:assert/strict'
import {Resvg} from '@resvg/resvg-js'
const root=resolve(import.meta.dirname,'..'),raw=readFileSync(resolve(root,'dist/index/circuit.json')),c=JSON.parse(raw)
const z=unzipSync(readFileSync(resolve(root,'dist/assembly-review/native-export.zip')))
const paste=new TextDecoder().decode(z['F_Paste.gbr'])
if(!paste.includes('%MOMM*%')||!paste.includes('%FSLAX46Y46*%'))throw Error('Unrecognized native Gerber units/format')
const apertures=new Map([...paste.matchAll(/%ADD(\d+)R,([\d.]+)X([\d.]+)\*%/g)].map(m=>[m[1],[Number(m[2]),Number(m[3])]]))
const flashes=[];let x=0,y=0,a,rotation=0
for(const l of paste.split('\n')){
 const d=l.match(/^D(\d+)\*$/);if(d)a=d[1]
 const lr=l.match(/^%LR(-?[\d.]+)\*%$/);if(lr)rotation=Number(lr[1])
 const m=l.match(/^(?:X(-?\d+))?(?:Y(-?\d+))?D0([123])\*$/);if(!m)continue
 if(m[1])x=Number(m[1])/1e6;if(m[2])y=Number(m[2])/1e6
 if(m[3]==='3'){const size=apertures.get(a);if(size)assert.equal(rotation%90,0,'Non-cardinal rectangle requires a polygon check');flashes.push({x,y,size:size&&Math.abs(rotation%180)===90?[size[1],size[0]]:size})}
}
function pad(name,pin){const sc=c.find(r=>r.type==='source_component'&&r.name===name);const sp=c.find(r=>r.type==='source_port'&&r.source_component_id===sc.source_component_id&&r.pin_number===pin);const pp=c.find(r=>r.type==='pcb_port'&&r.source_port_id===sp.source_port_id);return c.find(r=>r.type==='pcb_smtpad'&&r.pcb_port_id===pp.pcb_port_id)}
const u11=pad('U11',15),u7=pad('U7',37)
const f11=flashes.filter(f=>Math.hypot(f.x-u11.x,f.y-u11.y)<.002)
if(f11.length!==1||!f11[0].size||Math.abs(f11[0].size[0]-2.59)>.001||Math.abs(f11[0].size[1]-2.6)>.001)throw Error('U11 native stencil does not match the 2.59 x 2.60 mm board-axis opening')
const f7=flashes.filter(f=>Math.abs(f.x-u7.x)<u7.width/2&&Math.abs(f.y-u7.y)<u7.height/2)
const spec=JSON.parse(readFileSync(resolve(root,'manufacturing/stencil-u7.json')))
const close=(a,b)=>Math.abs(a-b)<.000002
assert.equal(f7.length,6,'U7 must export exactly six paste windows, without the old central opening')
assert.ok(close(u7.width,spec.copperSizeMm[0])&&close(u7.height,spec.copperSizeMm[1]),'U7 continuous exposed copper pad changed')
const native=c.filter(r=>r.type==='pcb_solder_paste'&&r.pcb_component_id===u7.pcb_component_id&&Math.abs(r.x-u7.x)<u7.width/2&&Math.abs(r.y-u7.y)<u7.height/2)
assert.equal(native.length,6,'U7 must contain six native paste records')
for(const expected of spec.apertures){
 const x=u7.x+expected.centerX,y=u7.y+expected.centerY
 const match=f7.filter(f=>close(f.x,x)&&close(f.y,y)&&f.size&&close(f.size[0],expected.width)&&close(f.size[1],expected.height))
 assert.equal(match.length,1,`Missing/malformed U7 native Gerber aperture at ${x},${y}`)
 assert.equal(native.filter(r=>r.layer==='top'&&r.shape==='rect'&&!r.pcb_smtpad_id&&close(r.x,x)&&close(r.y,y)&&close(r.width,expected.width)&&close(r.height,expected.height)).length,1,'U7 window must come from a standalone native solderpaste element')
}
const coverage=f7.reduce((a,f)=>a+f.size[0]*f.size[1],0)/(u7.width*u7.height)*100
const q7=pad('Q7',5)
const q7Paste=c.filter(r=>r.type==='pcb_solder_paste'&&r.pcb_component_id===q7.pcb_component_id&&!r.pcb_smtpad_id).map(r=>{
 assert.ok(r.shape==='rect'||r.shape==='rotated_rect');const a=r.ccw_rotation??0;assert.equal(a%90,0);return {...r,shape:'rect',width:Math.abs(a%180)===90?r.height:r.width,height:Math.abs(a%180)===90?r.width:r.height}
})
assert.equal(q7Paste.length,8,'Q7 requires four drain windows and four finger apertures')
assert.ok(!c.some(r=>r.type==='pcb_solder_paste'&&r.pcb_smtpad_id===q7.pcb_smtpad_id),'Q7 automatic polygon paste must be absent')
for(const r of q7Paste){
 assert.equal(r.shape,'rect')
 assert.equal(flashes.filter(f=>close(f.x,r.x)&&close(f.y,r.y)&&f.size&&close(f.size[0],r.width)&&close(f.size[1],r.height)).length,1,'Q7 native rectangular aperture missing from Gerber')
}
// Independently check each generated window stays inside the continuous
// concave drain copper and that separate windows do not overlap.
const cross=(a,b,p)=>(b.x-a.x)*(p.y-a.y)-(b.y-a.y)*(p.x-a.x)
const pointInside=(p,poly)=>{let inside=false;for(let i=0,j=poly.length-1;i<poly.length;j=i++){const a=poly[i],b=poly[j];if((a.y>p.y)!==(b.y>p.y)&&p.x<(b.x-a.x)*(p.y-a.y)/(b.y-a.y)+a.x)inside=!inside}return inside}
for(const r of q7Paste){
 const corners=[{x:r.x-r.width/2,y:r.y-r.height/2},{x:r.x+r.width/2,y:r.y-r.height/2},{x:r.x+r.width/2,y:r.y+r.height/2},{x:r.x-r.width/2,y:r.y+r.height/2}]
 assert.ok(corners.every(p=>pointInside(p,q7.points)),'Q7 paste corner outside copper')
 for(let i=0;i<4;i++)for(let j=0;j<q7.points.length;j++){
  const a=corners[i],b=corners[(i+1)%4],c=q7.points[j],d=q7.points[(j+1)%q7.points.length]
  assert.ok(!(cross(a,b,c)*cross(a,b,d)<-1e-12&&cross(c,d,a)*cross(c,d,b)<-1e-12),'Q7 paste crosses a copper notch')
 }
}
for(let i=0;i<q7Paste.length;i++)for(let j=i+1;j<q7Paste.length;j++){
 const a=q7Paste[i],b=q7Paste[j]
 assert.ok(Math.abs(a.x-b.x)>=(a.width+b.width)/2||Math.abs(a.y-b.y)>=(a.height+b.height)/2,'Overlapping Q7 paste windows')
}
const polyArea=Math.abs(q7.points.reduce((sum,p,i)=>{const n=q7.points[(i+1)%q7.points.length];return sum+p.x*n.y-n.x*p.y},0))/2
const q7Coverage=q7Paste.reduce((sum,r)=>sum+r.width*r.height,0)/polyArea*100
const report={status:'PASS_NATIVE_GEOMETRY_FACTORY_PROCESS_REVIEW_REQUIRED',pcbSha256:createHash('sha256').update(raw).digest('hex'),
 Q7:{status:'PASS_EIGHT_RECTANGULAR_APERTURES',pasteCoveragePercent:q7Coverage,factoryReview:'Trial pattern: four 1.05 x 0.70 mm drain windows and four 0.35 x 0.49 mm drain-finger apertures. Unchanged continuous polygon copper. Factory must approve stencil thickness, release, rounding and solder coverage.'},
 U11:{status:'PASS',pasteMm:f11[0].size,copperMm:[u11.width,u11.height],method:'Native smtpad solderPasteMargin=0; TI PWP0014K 0.125 mm stencil example'},
 U7:{status:'PASS_SIX_RECTANGULAR_WINDOWS',nativeFlashes:f7,copperMm:[u7.width,u7.height],pasteCoveragePercent:coverage,method:'Six native solderpaste elements; continuous smtpad with solderPasteMargin=-2 suppresses only its automatic paste',factoryReview:'Native windows have square corners. Review R0.05 corner rounding and 0.125 mm stencil with factory; see stencil-u7.json.'},
 note:'This verifies unmodified native Gerber geometry, not factory process approval. No Gerbers were changed.'}
writeFileSync(resolve(root,'dist/assembly-review/native-stencil-review.json'),JSON.stringify(report,null,2)+'\n')
// Preview the parsed Gerber flashes over the actual continuous copper land.
const scale=120,cx=300,cy=250
const rect=(x,y,w,h,fill,extra='')=>`<rect x="${cx+(x-u7.x-w/2)*scale}" y="${cy-(y-u7.y+h/2)*scale}" width="${w*scale}" height="${h*scale}" fill="${fill}" ${extra}/>`
const svg=`<svg xmlns="http://www.w3.org/2000/svg" width="600" height="500" viewBox="0 0 600 500"><rect width="600" height="500" fill="white"/><g font-family="sans-serif" fill="#152536"><text x="30" y="38" font-size="24">U7 — native top-paste export</text><text x="30" y="65" font-size="16">Six 1.05 × 1.15 mm rectangular windows</text></g>${rect(u7.x,u7.y,u7.width,u7.height,'#d9e3ec','stroke="#547083" stroke-width="2"')}${f7.map(f=>rect(f.x,f.y,...f.size,'#2d7983')).join('')}<g font-family="sans-serif" fill="#152536" font-size="16"><text x="30" y="442">Continuous copper: 3.650 × 2.650 mm</text><text x="30" y="470">Paste area: ${coverage.toFixed(2)}% · factory process review required</text></g></svg>`
writeFileSync(resolve(root,'dist/assembly-review/u7-native-paste.svg'),svg)
writeFileSync(resolve(root,'dist/assembly-review/u7-native-paste.png'),new Resvg(svg).render().asPng())
console.log('Q7 native eight-window stencil: PASS ('+q7Coverage.toFixed(2)+'%); U11 native stencil: PASS; U7 native six-window export: PASS (rectangular, '+coverage.toFixed(2)+'% area)')