imrishabh18/nema-23-stepper-controller
A four-layer NEMA23 stepper motor controller PCB featuring an RP2040 MCU, DRV8452 dual-phase driver, USB‑C PD power input/protection, current and temperature sensing, status LED/buzzer, and optional magnetic encoder.
- Version
- 1.3.2
- License
- unset
- Stars
- 1
scripts/check-selected-footprints.mjs
import { readFileSync,writeFileSync } from 'node:fs'
import { createHash } from 'node:crypto'
import assert from 'node:assert/strict'
import { Circuit } from 'tscircuit'
import { createElement } from 'react'
import { CH224A } from '../engineering/easyeda/C42459160.imported'
import { TPS26630RGET } from '../engineering/easyeda/C2864391.imported'
const manifest=JSON.parse(readFileSync('engineering/easyeda/selected-parts-manifest.json'))
assert.equal(manifest.converter,JSON.parse(readFileSync('package.json')).devDependencies.easyeda)
for(const part of Object.values(manifest.parts))for(const [file,hash] of Object.entries(part.sourceFiles))assert.equal(createHash('sha256').update(readFileSync(file)).digest('hex'),hash,`Supplier source changed: ${file}`)
const json=JSON.parse(readFileSync('dist/index/circuit.json'))
const checks=[]
for(const [name,mpn,id,Component,padCount] of [['U_PD','CH224A','C42459160',CH224A,11],['U_EFUSE','TPS26630RGET','C2864391',TPS26630RGET,25]]) {
const source=json.find(e=>e.type==='source_component'&&e.name===name)
assert.equal(source.manufacturer_part_number,mpn);assert.deepEqual(source.supplier_part_numbers.jlcpcb,[id])
const placed=json.find(e=>e.type==='pcb_component'&&e.source_component_id===source.source_component_id)
const actual=json.filter(e=>e.type==='pcb_smtpad'&&e.pcb_component_id===placed.pcb_component_id)
const c=new Circuit();c.add(createElement('board',{width:20,height:20,routingDisabled:true},createElement(Component,{name:'REFERENCE',pcbX:0,pcbY:0,pcbRotation:0})));await c.renderUntilSettled()
const ref=c.getCircuitJson().filter(e=>e.type==='pcb_smtpad')
assert.equal(actual.length,padCount);assert.equal(ref.length,padCount)
const rad=placed.rotation*Math.PI/180,cos=Math.round(Math.cos(rad)),sin=Math.round(Math.sin(rad))
for(const pad of ref){
const target=actual.find(e=>e.port_hints[0]===pad.port_hints[0]);assert.ok(target)
const padRad=(pad.ccw_rotation??0)*Math.PI/180,refCos=Math.abs(Math.round(Math.cos(padRad))),refSin=Math.abs(Math.round(Math.sin(padRad)))
const refWidth=refCos*pad.width+refSin*pad.height,refHeight=refSin*pad.width+refCos*pad.height
const expected={x:placed.center.x+pad.x*cos-pad.y*sin,y:placed.center.y+pad.x*sin+pad.y*cos,width:Math.abs(cos)*refWidth+Math.abs(sin)*refHeight,height:Math.abs(sin)*refWidth+Math.abs(cos)*refHeight}
// Core swaps dimensions for axis-aligned rectangles but stores a rotation
// with the original dimensions for pills. Compare the physical geometry.
const targetRad=(target.ccw_rotation??0)*Math.PI/180,targetCos=Math.abs(Math.round(Math.cos(targetRad))),targetSin=Math.abs(Math.round(Math.sin(targetRad)))
const physical={x:target.x,y:target.y,width:targetCos*target.width+targetSin*target.height,height:targetSin*target.width+targetCos*target.height}
for(const [field,value] of Object.entries(expected))assert.ok(Math.abs(physical[field]-value)<1e-7,`${name}.${pad.port_hints[0]} supplier pad ${field} mismatch`)
assert.equal(target.shape.replace('rotated_',''),pad.shape.replace('rotated_',''));assert.equal(target.layer,'top')
if(pad.radius!==undefined)assert.equal(target.radius,pad.radius)
}
assert.ok(placed.supplier_pin1_location_map?.jlcpcb,`${name}: supplier orientation unavailable`)
assert.equal(placed.pin1_location,placed.supplier_pin1_location_map.jlcpcb,`${name}: supplier baseline orientation differs`)
checks.push({reference:name,mpn,jlcpcb:id,package:manifest.parts[id].supplierPackage,padCount,allPadsMatchExactSupplierImport:true,rotation:placed.rotation,pin1Baseline:placed.pin1_location,supplierPin1Baseline:placed.supplier_pin1_location_map.jlcpcb})
}
const bulkSource=json.find(e=>e.type==='source_component'&&e.name==='C_BULK');assert.deepEqual(bulkSource.supplier_part_numbers.jlcpcb,['C178712'])
const bulk=json.find(e=>e.type==='pcb_component'&&e.source_component_id===bulkSource.source_component_id)
const pads=json.filter(e=>e.type==='pcb_plated_hole'&&e.pcb_component_id===bulk.pcb_component_id)
assert.equal(pads.length,2);assert.ok(Math.abs(Math.hypot(pads[0].x-pads[1].x,pads[0].y-pads[1].y)-5)<1e-7)
const supplier=JSON.parse(readFileSync('imports/footprint-data/C178712.json')).filter(e=>e.type==='pcb_plated_hole')
assert.equal(supplier.length,2);const supplierPitch=Math.hypot(supplier[0].x-supplier[1].x,supplier[0].y-supplier[1].y);assert.ok(Math.abs(supplierPitch-5.08)<1e-5,'Review changed supplier capacitor pitch');
// Panasonic specifies 5.0 mm nominal. Supplier CAD rounds this to 0.2 inch (5.08 mm); retain the datasheet nominal board pitch.
assert.ok(Math.abs(supplierPitch-5)<0.1)
for(const pad of pads)assert.ok(Math.abs(pad.hole_diameter-1)<1e-7,'Bulk lead hole must match selected supplier footprint')
assert.ok(bulk.supplier_pin1_location_map?.jlcpcb,'Bulk supplier orientation unavailable')
checks.push({reference:'C_BULK',jlcpcb:'C178712',leadPitchMm:5,supplierCadPitchMm:supplierPitch,holeDiameterMm:1,supplierPin1Baseline:bulk.supplier_pin1_location_map.jlcpcb,boardPin1Baseline:bulk.pin1_location,bodyDiameterMm:10,bodyHeightMm:25})
writeFileSync('checks/selected-part-footprints.json',JSON.stringify({status:'passed',checkedCircuitSha256:createHash('sha256').update(readFileSync('dist/index/circuit.json')).digest('hex'),checks},null,2)+'\n')
console.log('PASS: CH224A and RGET exact supplier lands/pin numbering/orientation; C178712 lead pitch and holes. Check the exported assembly preview before ordering.')