shibosoftwaredev/f1c100s-linux-nema17-stepper-controller
A four-layer NEMA 17 motor-controller PCB combining an Allwinner F1C100S Linux computer, USB‑PD/buck power system, W5500 Ethernet, STM32 real-time motion control, DRV8825 stepper driver, protection/temperature/current telemetry, USB interfaces, and local LED/buzzer diagnostics.
- Version
- 0.4.56
- License
- unset
- Stars
- 0
scripts/check-fix-regression.mjs
import fs from "node:fs"
import assert from "node:assert/strict"
import crypto from "node:crypto"
const baselinePath="../../checkpoints/nema17-review-fixes-20261019-3wRAM9/before.circuit.json"
const a=JSON.parse(fs.readFileSync(baselinePath,"utf8"))
const bytes=fs.readFileSync("dist/index/circuit.json"),b=JSON.parse(bytes)
const stable=x=>JSON.stringify(x,(_,v)=>typeof v==="number"?Math.round(v*1e8)/1e8:
v&&typeof v==="object"&&!Array.isArray(v)?Object.fromEntries(Object.entries(v).sort(([a],[b])=>a.localeCompare(b))):v)
const fields=["name","ftype","manufacturer_part_number","supplier_part_numbers","resistance","capacitance",
"inductance","frequency","load_capacitance","max_voltage_rating","standard","pin_count"]
const parts=c=>c.filter(e=>e.type==="source_component").map(e=>Object.fromEntries(fields.filter(k=>e[k]!==undefined).map(k=>[k,e[k]])))
.sort((a,b)=>a.name.localeCompare(b.name))
const before=parts(a),after=parts(b)
const code=(before,after)=>({supplier_part_numbers:{before:{jlcpcb:[before]},after:{jlcpcb:[after]}}})
const approved={
R_REF_TOP:code("C25742","C163450"),
R_ETH_TXN:code("C25792","C87044"),R_ETH_TXP:code("C25792","C87044"),
R_ETH_RXN:code("C25792","C87044"),R_ETH_RXP:code("C25792","C87044"),
C_ETH_RXN_AC:code("C1590","C2760541"),C_ETH_RXP_AC:code("C1590","C2760541"),
R_PD_GOOD_PU:{resistance:{before:49900,after:51000}},
C_VM_BULK:{...code("C3018707","C41417029"),
manufacturer_part_number:{before:"EEFCX1E220R",after:"CA55-D050M226TE100"},
max_voltage_rating:{before:25,after:50}},
}
for(const [name,changes]of Object.entries(approved)){
const x=before.find(p=>p.name===name),y=after.find(p=>p.name===name)
assert.ok(x&&y)
for(const [field,values]of Object.entries(changes)){
assert.deepEqual(x[field],values.before,`${name} original ${field}`)
assert.deepEqual(y[field],values.after,`${name} intended ${field}`)
x[field]=y[field]
}
}
assert.equal(stable(before),stable(after),"Only explicitly approved BOM corrections may change original parts")
function nets(c){
const names=new Map(c.filter(e=>e.type==="source_component").map(e=>[e.source_component_id,e.name]))
const parent=new Map(),root=id=>{if(!parent.has(id))parent.set(id,id);if(parent.get(id)!==id)parent.set(id,root(parent.get(id)));return parent.get(id)}
for(const e of c){const ids=e.type==="source_trace"?[...e.connected_source_port_ids??[],...e.connected_source_net_ids??[]]:e.type==="source_component_internal_connection"?e.source_port_ids:[]
for(const id of ids.slice(1))parent.set(root(id),root(ids[0]))}
const groups=new Map()
for(const p of c.filter(e=>e.type==="source_port")){const n=root(p.source_port_id);if(!groups.has(n))groups.set(n,[]);groups.get(n).push(`${names.get(p.source_component_id)}.${p.pin_number??p.name}`)}
return [...groups.values()].map(g=>g.sort()).sort((a,b)=>a.join().localeCompare(b.join()))
}
assert.equal(stable(nets(a)),stable(nets(b)),"Every pre-fix electrical net must be preserved, including both current channels")
const board=c=>c.find(e=>e.type==="pcb_board")
assert.equal(stable(board(a)),stable(board(b)),"Board outline/layers/rules must not change")
const placement=c=>{
const src=new Map(c.filter(e=>e.type==="source_component").map(e=>[e.source_component_id,e.name]))
return Object.fromEntries(c.filter(e=>e.type==="pcb_component").map(e=>[src.get(e.source_component_id),
{center:e.center,rotation:e.rotation,layer:e.layer,width:e.width,height:e.height,doNotPlace:e.do_not_place}]))
}
const pa=placement(a),pb=placement(b)
for(const [name,x]of [["PE7",5.4],["PE5",6.2]]){
assert.equal(stable(pa[name].center),stable({x,y:-.2}),`${name} original debug-pad position`)
assert.equal(stable(pb[name].center),stable({x,y:-.14}),`${name} approved 0.06 mm debug-pad separation fix`)
pa[name].center=pb[name].center
}
const approvedPlacement={
C_SV_H:{before:{x:-.1,y:-4.45},after:{x:-.08,y:-4.45}},
C_D50:{before:{x:8.9,y:3.8},after:{x:8.9,y:3.83}},
PE2:{before:{x:8.24999,y:2.599956},after:{x:8.278274,y:2.571672}},
U_PD:{before:{x:15,y:1},after:{x:14.985,y:1}},
C_B_AVCC:{before:{x:0,y:14},after:{x:0,y:13.98}},
R_PD_STATUS_PULLUP:{before:{x:19.25,y:3.75},after:{x:19.22,y:3.75}},
C_PD_ADC:{before:{x:18,y:8},after:{x:18.2,y:8.18}},
}
for(const [name,{before,after}]of Object.entries(approvedPlacement)){
assert.equal(stable(pa[name].center),stable(before),`${name} original position`)
assert.equal(stable(pb[name].center),stable(after),`${name} approved drill/pad separation fix`)
pa[name].center=pb[name].center
}
// Round 3 rotates this one filter capacitor to open a real drill-safe corridor
// between the PMOS lands and the bottom-side telemetry network. No other
// component, connector, mounting hole, or board dimension may rotate or move.
assert.equal(pa.C_PD_ADC.rotation,0)
assert.equal(pb.C_PD_ADC.rotation,90)
assert.equal(stable({width:pa.C_PD_ADC.width,height:pa.C_PD_ADC.height}),stable({width:1.56,height:.64}))
assert.equal(stable({width:pb.C_PD_ADC.width,height:pb.C_PD_ADC.height}),stable({width:.64,height:1.56}))
for(const key of ["rotation","width","height"])pa.C_PD_ADC[key]=pb.C_PD_ADC[key]
assert.equal(stable(pa),stable(pb),"Only the explicitly approved debug-pad move may change placement")
const vias=b.filter(e=>e.type==="pcb_via")
assert.ok(vias.every(v=>v.hole_diameter===.3&&v.outer_diameter===.45&&v.layers.length===4))
assert.equal(vias.length,a.filter(e=>e.type==="pcb_via").length)
assert.equal(b.filter(e=>e.type.endsWith("_error")).length,0,"Native errors must remain zero")
for(const file of ["firmware/stm32/main.c","firmware/stm32/motion.c","firmware/stm32/motion.h","firmware/motor-control.py"])
assert.equal(fs.readFileSync(file,"utf8"),fs.readFileSync(`reports/published-comparison/checkpoint-${file.replaceAll("/","_")}`,"utf8"),`${file} unchanged`)
const report={checkedAt:new Date().toISOString(),circuitSha256:crypto.createHash("sha256").update(bytes).digest("hex"),
originalParts:after.length,originalNets:nets(b).length,approvedBomCorrections:Object.keys(approved),
unchangedTopology:true,approvedPlacementChanges:[{name:"PE7",dy:.06},{name:"PE5",dy:.06},
...Object.entries(approvedPlacement).map(([name,positions])=>({name,...positions}))],
unchangedOtherPlacements:true,unchangedBoardAndRules:true,unchangedFirmware:true,
throughVias:vias.length,viaHole:.3,viaPad:.45,nativeErrors:0}
fs.writeFileSync("reports/fix-regression.json",JSON.stringify(report,null,2)+"\n")
console.log(JSON.stringify(report))