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/compare-round9-live-latest.mjs

import fs from "node:fs"
import crypto from "node:crypto"
import assert from "node:assert/strict"
import {parseJlcProduct} from "./audit-jlcpcb-stock.mjs"

const dir="reports/round9-live-latest"
const read=p=>JSON.parse(fs.readFileSync(p,"utf8"))
const hash=p=>crypto.createHash("sha256").update(fs.readFileSync(p)).digest("hex")
const remote=read(`${dir}/fetch-manifest.json`)
const before=read(`${dir}/latest-dist_index_circuit.json`),after=read("dist/index/circuit.json")
const status=read("reports/round9-fix-status.json")
assert.equal(hash("dist/index/circuit.json"),status.circuitSha256)
for(const [p,h]of Object.entries(status.evidence))assert.equal(hash(p),h,`fresh local evidence: ${p}`)
const of=(c,t)=>c.filter(e=>e.type===t)
const names=c=>new Map(of(c,"source_component").map(e=>[e.source_component_id,e.name]))
const parts=c=>new Map(of(c,"source_component").map(e=>[e.name,e]))
const b=parts(before),a=parts(after)
const added=[...a.keys()].filter(n=>!b.has(n)).sort(),removed=[...b.keys()].filter(n=>!a.has(n))
assert.deepEqual(added,["C_USB_GATE","Q_USB_ATTACH","R_USB_BASE","R_USB_BASE_PD","R_USB_OE_PU","U_USB_GATE"].sort())
assert.deepEqual(removed,[])
const replacements=new Set(["C_CURRENT_A_SUPPLY","C_CURRENT_B_SUPPLY"])
const bomChanges=[]
for(const [name,p]of b){
  const q=a.get(name)
  for(const k of ["resistance","capacitance","inductance","frequency","load_capacitance","standard"])
    assert.deepEqual(q[k],p[k],`${name}.${k} preserved`)
  if(replacements.has(name)){
    assert.equal(p.capacitance,100e-9);assert.equal(q.capacitance,100e-9)
    assert.equal(p.max_voltage_rating,16);assert.equal(q.max_voltage_rating,10)
    assert.deepEqual(p.supplier_part_numbers.jlcpcb,["C18164586"])
    assert.deepEqual(q.supplier_part_numbers.jlcpcb,["C7393837"])
    bomChanges.push({name,oldMpn:p.manufacturer_part_number,newMpn:q.manufacturer_part_number,
      oldCode:"C18164586",newCode:"C7393837",oldRatingV:16,newRatingV:10,railV:3.3,sameCapacitance:true})
  }else for(const k of ["manufacturer_part_number","supplier_part_numbers","max_voltage_rating"])
    assert.deepEqual(q[k],p[k],`${name}.${k} retained`)
}
function partition(c){
  const ns=names(c),parents=new Map()
  const root=id=>{if(!parents.has(id))parents.set(id,id);if(parents.get(id)!==id)parents.set(id,root(parents.get(id)));return parents.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))parents.set(root(id),root(ids[0]))
  }
  return new Map(of(c,"source_port").map(p=>[`${ns.get(p.source_component_id)}.${p.pin_number??p.name}`,root(p.source_port_id)]))
}
const bp=partition(before),ap=partition(after)
const approved=new Set(["J_USB_DATA.15","J_USB_DATA.16","U_USB_ESD.4","U_USB_ESD.5","U_USB_ESD.6","C_ESD.1"])
const unaffected=[...bp.keys()].filter(n=>!approved.has(n))
for(const p of bp.keys())assert.ok(ap.has(p),`original terminal retained: ${p}`)
function groups(p){const m=new Map();for(const n of unaffected){const k=p.get(n);if(!m.has(k))m.set(k,[]);m.get(k).push(n)}return [...m.values()].map(a=>a.sort().join("|")).sort()}
assert.deepEqual(groups(ap),groups(bp),"all original non-USB net partitions, including current sensing, retained")
const host=["J_USB_DATA.15","J_USB_DATA.16","U_USB_ESD.5","C_ESD.1","R_USB_BASE.1"]
assert.equal(new Set(host.map(n=>ap.get(n))).size,1)
assert.deepEqual([...ap].filter(([,r])=>r===ap.get(host[0])).map(([n])=>n).sort(),host.sort())
const placed=c=>{const ns=names(c);return new Map(of(c,"pcb_component").map(e=>[ns.get(e.source_component_id),e]))}
const bpc=placed(before),apc=placed(after)
const critical=["U1","U_MOTION","U_MOTOR","U_PD","J_USB_DATA","J_USB_PWR","J_ETH","J_MOTOR","SW_RESET","JP_BOOT"]
for(const n of critical)for(const k of ["center","rotation","layer","do_not_place"])assert.deepEqual(apc.get(n)[k],bpc.get(n)[k],`${n}.${k}`)
const shape=p=>({outline:p.outline,width:p.width,height:p.height,layers:p.num_layers})
assert.deepEqual(shape(of(after,"pcb_board")[0]),shape(of(before,"pcb_board")[0]))
const holes=c=>of(c,"pcb_hole").map(e=>({x:e.x,y:e.y,hole_diameter:e.hole_diameter,shape:e.shape})).sort((a,b)=>a.x-b.x||a.y-b.y)
assert.deepEqual(holes(after),holes(before))
const vias=of(after,"pcb_via")
assert.ok(vias.every(v=>v.hole_diameter===.3&&v.outer_diameter===.45&&v.layers.length===4))
let firmwareUnchanged=0
for(const f of remote.downloaded.filter(e=>e.file.startsWith("firmware/"))){assert.equal(hash(f.file),f.sha256,`firmware retained ${f.file}`);firmwareUnchanged++}
const oldWarnings=of(before,"pcb_trace_warning")
const key=e=>`${e.type}:${(e.message??"").replace(/#\d+\b/g,"#ID").replaceAll("0.12000000000000001","0.12")}`
const warningCounts=c=>{const m=new Map();for(const e of c.filter(e=>e.type.endsWith("_warning"))){const k=key(e);m.set(k,{count:(m.get(k)?.count??0)+1,example:e})}return m}
const bw=warningCounts(before),aw=warningCounts(after),newWarnings=[]
for(const [k,v]of aw){const extra=v.count-(bw.get(k)?.count??0);if(extra>0)newWarnings.push({count:extra,type:v.example.type,message:v.example.message,sourceTraceId:v.example.source_trace_id})}
const inventories=[]
for(const code of ["C32843","C85202","C18164586","C7393837"]){
  const url=`https://jlcpcb.com/partdetail/${code}`
  const p=Bun.spawn(["curl","--fail","--silent","--show-error","--location","--max-time","30",url],{stdout:"pipe",stderr:"pipe"})
  const [html,err,exit]=await Promise.all([new Response(p.stdout).text(),new Response(p.stderr).text(),p.exited])
  assert.equal(exit,0,`${code} ${err}`)
  const s=parseJlcProduct(html,code)
  inventories.push({code,checkedAt:new Date().toISOString(),url,...s,
    usedByLatest:[...b].filter(([,p])=>p.supplier_part_numbers?.jlcpcb?.includes(code)).map(([n])=>n),
    usedByLocal:[...a].filter(([,p])=>p.supplier_part_numbers?.jlcpcb?.includes(code)).map(([n])=>n)})
}
const oldStatus=read(`${dir}/latest-reports_round5-fix-status.json`)
const oldSolder=read(`${dir}/latest-reports_solder-process-audit.json`)
const newSolder=read("reports/solder-process-audit.json")
const drill=read(`${dir}/drill-comparison.json`)
assert.equal(drill.baselineSha256,hash(`${dir}/latest-dist_index_circuit.json`))
assert.equal(drill.circuitSha256,hash("dist/index/circuit.json"))
assert.equal(drill.currentFindings,0);assert.equal(drill.regressionCount,0)
assert.match(fs.readFileSync(`${dir}/latest-shorts.log`,"utf8"),/No shorts detected/)
assert.match(fs.readFileSync("reports/round9-shorts.log","utf8"),/No shorts detected/)
assert.deepEqual(after.filter(e=>e.type.endsWith("_error")),[])
const normalizedThermal=c=>c.map(e=>`${e.part}.${e.pin}:${e.layer}`).sort()
assert.deepEqual(normalizedThermal(newSolder.thermalPadDrillIntersections),normalizedThermal(oldSolder.thermalPadDrillIntersections))
const report={checkedAt:new Date().toISOString(),latest:{version:remote.latestVersion,releaseId:remote.latestReleaseId,
  circuitSha256:hash(`${dir}/latest-dist_index_circuit.json`),buildCompletedAt:remote.buildCompletedAt,buildError:remote.buildError},
  local:{circuitSha256:hash("dist/index/circuit.json"),checkpoint:"../../checkpoints/nema17-round9-validated-local-20261020-HTyidb/board-checkpoint.tar.gz"},
  improvements:{ordinaryPadViaConflicts:{latest:oldSolder.ordinaryPadDrillIntersections.length,local:newSolder.ordinaryPadDrillIntersections.length},
    moduleTests:{latest:oldStatus.checks.moduleTests,local:status.checks.moduleTests},
    usbHardwareHostAttachIsolationAdded:true,usbEsdFootprintCorrected:true,
    actualThicknessMm:{latest:of(before,"pcb_board")[0].thickness??"implicit",local:of(after,"pcb_board")[0].thickness},
    capacitorsReplacedWithStockedEquivalentValue:bomChanges},
  invariants:{originalPartsRemoved:removed,unchangedExistingNonUsbPorts:unaffected.length,allNonUsbNetPartitionsIdentical:true,
    motorAndBothCoilSenseRetained:true,firmwareFilesByteIdentical:firmwareUnchanged,
    criticalPlacementUnchanged:critical,outlineAndHolesUnchanged:true,fourLayers:true,fullStack03Drill045Pad:true},
  freshChecks:{allLayerGerberShorts:{latest:"pass",local:"pass"},drillFindings:{latest:drill.baselineFindings,local:drill.currentFindings,regressions:drill.regressionCount},
    localNativeErrors:0,previousLocalValidationEvidenceHashesMatch:true},
  changedOrUnqualified:{nativeWarnings:{latest:before.filter(e=>e.type.endsWith("_warning")).length,
    local:after.filter(e=>e.type.endsWith("_warning")).length,added:newWarnings},
    motorVmWidthChange:status.powerCopperChange,
    innerGroundFills:"Added on both inner layers; not qualified continuous reference planes/controlled impedance.",
    assemblySourcesChanged:["assembly-models.json","assembly-parts.json","assembly-metadata.ts"].filter(f=>hash(f)!==hash(`${dir}/latest-${f}`)),
    thermalPadDrillRecords:{latest:oldSolder.thermalPadDrillIntersections.length,local:newSolder.thermalPadDrillIntersections.length,qualified:false},
    actualMotorMountAndBenchQualification:"Still required in both versions"},
  liveInventory:inventories,
  evidence:Object.fromEntries([`${dir}/fetch-manifest.json`,`${dir}/latest-dist_index_circuit.json`,
    `${dir}/latest-shorts.log`,`${dir}/drill-comparison.json`,"reports/round9-fix-status.json"].map(p=>[p,hash(p)])),
  decision:{betterInMeasuredAreas:true,noRegressionsProven:false,tsciPushPerformed:false,
    reason:"Do not treat the increased warnings and VM trace-width reduction as an unqualified no-regression result. Full local verification also fails the live orderable-stock gate; stock shortages for the unchanged W5500/BSS84 affect both versions, not a newly introduced BOM regression.",
    publicationCondition:"User authorized push only if improved with no regressions; condition not proven."}}
fs.writeFileSync("reports/round9-vs-live-latest.json",JSON.stringify(report,null,2)+"\n")
console.log(JSON.stringify({latest:report.latest,improvements:report.improvements,invariants:report.invariants,
  warnings:report.changedOrUnqualified.nativeWarnings,inventory:inventories.map(p=>({code:p.code,displayed:p.displayedStock,orderable:p.orderableStock})),decision:report.decision}))