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-round12-regression.mjs

import fs from "node:fs"
import path from "node:path"
import crypto from "node:crypto"
import assert from "node:assert/strict"
const baseline = process.argv[2]
assert.ok(baseline, "Pass the restore-verified pre-fix board directory")
const read = p => JSON.parse(fs.readFileSync(p, "utf8"))
const hash = p => crypto.createHash("sha256").update(fs.readFileSync(p)).digest("hex")
const before = read(`${baseline}/dist/index/circuit.json`), after = read("dist/index/circuit.json")
const sha = hash("dist/index/circuit.json"), of = (c,t) => c.filter(e => e.type === t)
const normalize = value => {
  if (Array.isArray(value)) return value.map(normalize)
  if (value && typeof value === "object") return Object.fromEntries(Object.entries(value)
    .filter(([key]) => key !== "subcircuit_connectivity_map_key").map(([key,v]) => [key,normalize(v)]))
  return value
}
for (const type of ["source_net","source_component_internal_connection",
  "pcb_board","pcb_component","pcb_port","pcb_smtpad","pcb_plated_hole","pcb_hole",
  "pcb_keepout","pcb_courtyard_rect","pcb_courtyard_outline","pcb_courtyard_circle",
  "pcb_silkscreen_text","pcb_silkscreen_circle",...new Set(before.filter(e=>e.type.startsWith("cad_")).map(e=>e.type))])
  assert.deepEqual(normalize(of(after,type)), normalize(of(before,type)), `unchanged ${type}`)
const oldParts = new Map(of(before,"source_component").map(e => [e.name,e]))
assert.equal(of(after,"source_component").length,oldParts.size)
for (const part of of(after,"source_component")) {
  const old = oldParts.get(part.name)
  assert.ok(old, `no added part ${part.name}`)
  const expected = {...old}
  if (part.name === "Q_PD_ENABLE") {
    assert.equal(old.manufacturer_part_number,"BSS84-7-F")
    expected.manufacturer_part_number = "BSS84_R1_00001"
    expected.supplier_part_numbers = {jlcpcb:["C391434"]}
  }
  assert.deepEqual(normalize(part), normalize(expected), `only approved part metadata changes: ${part.name}`)
}
const oldTraces = of(before,"source_trace").filter(t=>t.name!=="BUCK_ENABLE")
const newTraces = of(after,"source_trace")
assert.equal(oldTraces.length,newTraces.length)
const remapId = id => {
  const number = Number(id.split("_").at(-1))
  assert.notEqual(number,104)
  return `source_trace_${number > 104 ? number-1 : number}`
}
for (const [i,trace] of oldTraces.entries())
  assert.deepEqual(normalize(newTraces[i]),normalize({...trace,source_trace_id:remapId(trace.source_trace_id)}),
    `connection and routing constraints preserved: ${trace.name ?? trace.source_trace_id}`)
const oldPorts = of(before,"source_port"), newPorts = of(after,"source_port")
assert.equal(oldPorts.length,newPorts.length)
for(const [i,port] of oldPorts.entries()) {
  const expected={...port}
  if(port.source_port_id==="source_port_255") expected.do_not_connect=true
  assert.deepEqual(normalize(newPorts[i]),normalize(expected),"only U_BUCK.EN gets an NC flag")
}
const oldReplay=read(`${baseline}/routing/complete-routes.json`)
const expectedReplay=oldReplay.filter(t=>t.source_trace_id!=="source_trace_104")
  .map(t=>({...t,source_trace_id:remapId(t.source_trace_id)}))
assert.deepEqual(read("routing/complete-routes.json"),expectedReplay,
  "only unsafe EN branch removed; every other explicit route byte retained, with trace IDs rebound")
const errors=after.filter(e=>e.type.endsWith("_error"))
assert.deepEqual(errors,[])
const warnings=after.filter(e=>e.type.endsWith("_warning"))
assert.equal(after.filter(e=>e.type==="schematic_element_outside_sheet_warning").length,0)
assert.ok(warnings.length<=before.filter(e=>e.type.endsWith("_warning")).length)
const warningKey=e=>`${e.type}:${(e.message??"").replace(/#\d+\b/g,"#ID").replace(/actual: [0-9.]+mm/g,"actual: WIDTH")}`
const remainingWarnings=before.filter(e=>e.type.endsWith("_warning"))
for(const warning of warnings) {
  const index=remainingWarnings.findIndex(old=>warningKey(old)===warningKey(warning))
  assert.ok(index>=0,`no new warning kind/source: ${warning.message}`)
  const [old]=remainingWarnings.splice(index,1)
  const actual=warning.message?.match(/actual: ([0-9.]+)mm/)
  const oldActual=old.message?.match(/actual: ([0-9.]+)mm/)
  if(actual&&oldActual)assert.ok(Number(actual[1])>=Number(oldActual[1])-1e-9,"no worse reported routing width")
}
const vias=of(after,"pcb_via")
assert.equal(vias.length,352)
assert.ok(vias.every(v=>v.hole_diameter===.3&&v.outer_diameter===.45&&
  ["top","inner1","inner2","bottom"].every(l=>v.layers.includes(l))&&v.layers.length===4))
const log = name => fs.readFileSync(`reports/round12-${name}.log`,"utf8")
assert.match(log("build"),/Circuits\s+1 passed/)
assert.match(log("shorts"),/No shorts detected/)
assert.doesNotMatch(log("style"),/<SchematicPlacementIssues>/)
assert.match(log("enable-tests"),/PASS: open EN/)
assert.match(log("board-tests"),/6 pass[\s\S]*0 fail/)
assert.match(log("module-tests"),/32 pass[\s\S]*0 fail/)
assert.match(log("motion-tests"),/PASS: signed coil conversion/)
assert.match(log("host-tests"),/Ran 6 tests[\s\S]*OK/)
assert.doesNotMatch(log("typecheck"),/error TS\d+|Error:|not found/)
assert.match(log("mechanical"),/0 cross-layer\/mechanical violations/)
const kelvin=JSON.parse(log("kelvin").trim())
assert.equal(kelvin.length,4)
assert.ok(kelvin.every(k=>k.connected&&k.joinsOnlyAtShuntLand))
assert.deepEqual(read("reports/round5-legend-audit.json").findings,[])
let unchangedSourceFiles=0
function unchanged(directory) {
  for(const entry of fs.readdirSync(`${baseline}/${directory}`,{withFileTypes:true})) {
    if(["node_modules","build","dist","reports",".git"].includes(entry.name))continue
    const p=path.join(directory,entry.name)
    if(entry.isDirectory())unchanged(p)
    else if(/\.(tsx?|mjs|json|c|h|py|md)$/.test(p)||entry.name==="Makefile") {
      assert.equal(hash(p),hash(`${baseline}/${p}`),`unchanged firmware/module source ${p}`)
      unchangedSourceFiles++
    }
  }
}
unchanged("modules");unchanged("firmware")
fs.writeFileSync("reports/module-test-regression.json",JSON.stringify({
  checkedAt:new Date().toISOString(),baselinePassed:32,baselineFailed:0,currentPassed:32,currentFailed:0,
  addedFailures:0,unchangedModuleAndFirmwareSources:unchangedSourceFiles,
  baselineCircuitSha256:hash(`${baseline}/dist/index/circuit.json`),
  currentLogSha256:hash("reports/round12-module-tests.log")},null,2)+"\n")
for(const filename of ["buck-enable-audit.json","connectivity-audit.json","coil-all-copper-preflight-meta.json","solder-process-audit.json",
  "drill-clearance-regression.json","additional-dfm-review.json","bom-validation.json","jlcpcb-stock-audit.json",
  "round12-width-proof.json","round12-release-gates.json"])
  assert.equal(read(`reports/${filename}`).circuitSha256,sha,`fresh evidence: ${filename}`)
const continuity=read("reports/connectivity-audit.json")
assert.equal(continuity.sourceFailures,0);assert.equal(continuity.disconnectedTerminalIslands,0)
assert.equal(continuity.emittedCopperOnly,true)
assert.equal(read("reports/coil-all-copper-preflight-meta.json").failureCount,0)
assert.equal(read("reports/coil-all-copper-preflight-meta.json").physicalCopperSha256,hash("routing/physical-copper.json"))
assert.equal(read("reports/drill-clearance-regression.json").currentFindings,0)
const widths=read("reports/round12-width-proof.json")
assert.equal(widths.vmBridgeMinimumMm,.8)
assert.equal(widths.vmBridgeOuterOnly,true)
assert.equal(widths.phaseBReturnMinimumMm,.3)
assert.equal(widths.reducedHalfMmVmSegmentsRemaining,0)
assert.deepEqual(read("reports/additional-dfm-review.json").findings,[])
assert.deepEqual(read("reports/bom-validation.json").findings,[])
const solder=read("reports/solder-process-audit.json")
assert.deepEqual(solder.ordinaryPadDrillIntersections,[])
assert.equal(solder.thermalPadDrillIntersections.length,16)
assert.equal(solder.approvedForAssembly,false)
const stock=read("reports/jlcpcb-stock-audit.json")
assert.deepEqual(stock.parts.filter(p=>p.status!=="in-stock").map(p=>p.code),["C32843"])
assert.equal(stock.parts.find(p=>p.code==="C391434").status,"in-stock")
const release=read("reports/round12-release-gates.json")
assert.equal(release.assemblyExportBlocked,true)
assert.equal(release.thermalProcessStillUnqualified,true)
assert.match(log("verify"),/all JLCPCB parts cover board quantity/)
const report={checkedAt:new Date().toISOString(),circuitSha256:sha,baselineCircuitSha256:hash(`${baseline}/dist/index/circuit.json`),
  localChangesNotPushed:true,fabricationReady:false,
  fixed:["TPS54302 EN isolated from 15 V VIN in both source and emitted copper; TI open-pin enable option used",
    "Q_PD_ENABLE changed to stocked PANJIT C391434 with checked G/S/D and compatible retained SOT-23 lands",
    "regulator capacitor sides, buck schematic, bypass grouping and factory wiring presentation improved",
    "contradictory README replaced; adapter and unconfirmed rear screw fit disclosed; history retained",
    "power audit source identities follow net names, not brittle render-order numbers"],
  invariants:{sizeMm:42.3,layers:4,thicknessMm:1.6,mountingHoles:4,mountPatternMm:31,
    placementsAndLandsUnchanged:true,cadAssemblyUnchanged:true,allNonEnCopperRoutesUnchanged:true,
    throughVias:352,viaDrillMm:.3,viaPadMm:.45,firmwareAndModuleSourcesUnchanged:unchangedSourceFiles},
  checks:{nativeErrors:0,nativeWarnings:warnings.length,baselineWarnings:153,localSchematicPlacementIssues:0,
    newWarningKindsOrWorseWidths:false,
    outsideSheetWarnings:0,shorts:"pass",emittedContinuity:"pass",independentCopper:"pass",drill:"pass",
    ordinaryPadDrillIntersections:0,thermalPadRecords:16,kelvin:"four pass",powerWidths:"pass",
    typecheck:"pass",boardTests:6,moduleTests:32,firmwareAndHost:"pass",bomCatalog:"pass",
    fullVerify:"blocked on W5500 orderable stock; no gate relaxed",assemblyExport:"blocked; negative release-gate tests pass"},
  remaining:["W5500 C32843 orderable-stock shortage; no unqualified network-chip substitution",
    "thermal via/stencil/opposite-face assembly-process approval",
    "actual motor rear endbell/bolt engagement/spacer/harness confirmation",
    "controlled USB/Ethernet impedance and return paths; buck feedback-loop placement review",
    "A-current output remains 86.72 mm: two bounded reroute candidates failed; neither installed",
    "complete Linux image, electrical first-article qualification and current/thermal calibration",
    "fresh website AI schematic analysis not run; local CLI and rendered sheets checked"],
  stockIssues:stock.parts.filter(p=>p.status!=="in-stock"),
  evidence:Object.fromEntries(["dist/index/circuit.json","routing/complete-routes.json","routing/physical-copper.json",
    ...["build","shorts","style","continuity","copper","drill","solder","kelvin","mechanical","dfm",
      "width-proof","typecheck","board-tests","module-tests","motion-tests","host-tests","enable-tests",
      "stock","bom","verify","gate-tests"].map(n=>`reports/round12-${n}.log`)].map(p=>[p,hash(p)]))}
fs.writeFileSync("reports/round12-fix-status.json",JSON.stringify(report,null,2)+"\n")
console.log(JSON.stringify({fixed:report.fixed,nativeErrors:0,nativeWarnings:warnings.length,
  schematicIssues:0,noUnintendedPhysicalChanges:true,remaining:report.remaining,localChangesNotPushed:true,fabricationReady:false}))