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/summarize-published-regression.mjs

import fs from "node:fs"

const dir = "reports/published-comparison"
const read = file => JSON.parse(fs.readFileSync(file, "utf8"))
const before = read(`${dir}/latest-dist_index_circuit.json`)
const after = read(`${dir}/checkpoint-dist_index_circuit.json`)
const local = read("dist/index/circuit.json")
const stable = value => JSON.stringify(value, (_, item) => typeof item === "number"
  ? Math.round(item * 1e9) / 1e9
  : item && typeof item === "object" && !Array.isArray(item)
    ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a.localeCompare(b))) : item)
function summarize(circuit) {
  const of = type => circuit.filter(e => e.type === type)
  const sources = new Map(of("source_component").map(e => [e.source_component_id, e]))
  const placed = Object.fromEntries(of("pcb_component").map(e => [sources.get(e.source_component_id).name, {
    center: e.center, rotation: e.rotation, layer: e.layer, width: e.width,
    height: e.height, doNotPlace: e.do_not_place,
  }]))
  const warnings = circuit.filter(e => e.type.endsWith("_warning"))
  const sourceTraces = new Map(of("source_trace").map(e => [e.source_trace_id, e]))
  function routeMetrics(name) {
    const source = [...sourceTraces.values()].find(e => e.name === name)
    if (!source) return null
    const segments = new Map(), vias = new Set()
    for (const trace of of("pcb_trace").filter(e => e.source_trace_id === source.source_trace_id)) {
      for (const point of trace.route) if (point.route_type === "via") vias.add(`${point.x},${point.y}`)
      for (let i = 1; i < trace.route.length; i++) {
        const a = trace.route[i - 1], b = trace.route[i]
        if (a.route_type !== "wire" || b.route_type !== "wire" || a.layer !== b.layer) continue
        const length = Math.hypot(b.x - a.x, b.y - a.y)
        if (length < 1e-7) continue
        const ends = [[a.x, a.y], [b.x, b.y]].map(e => stable(e)).sort()
        segments.set(stable([a.layer, ...ends]), { length, width: Math.max(a.width, b.width) })
      }
    }
    return { deduplicatedWireLengthMm: Number([...segments.values()].reduce((sum, s) => sum + s.length, 0).toFixed(3)),
      vias: vias.size, minSegmentWidth: Math.min(...[...segments.values()].map(s => s.width)) }
  }
  const mounted = of("pcb_component").filter(e => !e.do_not_place &&
    sources.get(e.source_component_id)?.ftype !== "simple_test_point")
  return { board: of("pcb_board")[0], placed, mounted: mounted.length, errors: circuit.filter(e => e.type.endsWith("_error")).length,
    warningCount: warnings.length, warnings: warnings.map(e => ({ type: e.type,
      message: e.message?.replace(/#\d+\b/g, "#ID") })),
    viaCount: of("pcb_via").length,
    viasCompliant: of("pcb_via").every(e => e.hole_diameter === .3 && e.outer_diameter === .45 &&
      e.layers?.length === 4 && ["top", "inner1", "inner2", "bottom"].every(l => e.layers.includes(l))),
    routes: Object.fromEntries(["MOTION_DIR", "MOTION_STEP", "CURRENT_A_OUTPUT", "CURRENT_B_OUTPUT",
      "CURRENT_B_FILTER", "CURRENT_A_DECOUPLE", "CURRENT_B_DECOUPLE"].map(name => [name, routeMetrics(name)])),
    parts: [...sources.values()].map(e => e.name) }
}
const a = summarize(before), b = summarize(after)
const shape = board => ({ outline: board.outline, width: board.width, height: board.height,
  layers: board.num_layers, thickness: board.thickness })
const moved = Object.keys(a.placed).filter(name => b.placed[name] && stable(a.placed[name]) !== stable(b.placed[name]))
const critical = ["U1", "U_MOTION", "U_MOTOR", "U_PD", "J_USB_DATA", "J_USB_PWR", "J_ETH", "J_MOTOR",
  "SW_RESET", "JP_BOOT", "MH1", "MH2", "MH3", "MH4"].filter(name => a.placed[name])
const count = items => {
  const counts = new Map()
  for (const item of items) { const key = stable(item); counts.set(key, (counts.get(key) ?? 0) + 1) }
  return counts
}
const oldWarnings = count(a.warnings), newWarnings = count(b.warnings)
const addedWarnings = [...newWarnings].flatMap(([key, amount]) =>
  Array.from({ length: Math.max(0, amount - (oldWarnings.get(key) ?? 0)) }, () => JSON.parse(key)))
const removedWarnings = [...oldWarnings].flatMap(([key, amount]) =>
  Array.from({ length: Math.max(0, amount - (newWarnings.get(key) ?? 0)) }, () => JSON.parse(key)))
const copperTypes = new Set(["pcb_board", "pcb_component", "pcb_port", "pcb_trace", "pcb_via", "pcb_smtpad",
  "pcb_hole", "pcb_plated_hole", "pcb_copper_pour", "pcb_keepout"])
const copper = c => c.filter(e => copperTypes.has(e.type)).map(stable).sort()
const assemblyFiles = ["assembly-models.json", "assembly-parts.json", "assembly-metadata.ts"]
const report = {
  inputs: read(`${dir}/remote-comparison-inputs.json`),
  unchangedBoardGeometry: stable(shape(a.board)) === stable(shape(b.board)),
  criticalComponents: Object.fromEntries(critical.map(name => [name, stable(a.placed[name]) === stable(b.placed[name])])),
  unchangedAssemblySources: Object.fromEntries(assemblyFiles.map(file => [file,
    fs.readFileSync(`${dir}/latest-${file}`, "utf8") === fs.readFileSync(`${dir}/checkpoint-${file}`, "utf8")])),
  existingPlacementChanges: moved.map(name => ({ name, before: a.placed[name], after: b.placed[name] })),
  addedParts: b.parts.filter(name => !a.parts.includes(name)), removedParts: a.parts.filter(name => !b.parts.includes(name)),
  errors: { latest: a.errors, checkpoint: b.errors }, warnings: { latest: a.warningCount, checkpoint: b.warningCount,
    added: addedWarnings, removed: removedWarnings },
  mountedParts: { latest: a.mounted, checkpoint: b.mounted },
  vias: { latest: a.viaCount, checkpoint: b.viaCount, bothCompliant: a.viasCompliant && b.viasCompliant },
  routes: { latest: a.routes, checkpoint: b.routes },
  publishedCopperMatchesLocallyAuditedCopper: stable(copper(after)) === stable(copper(local)),
  numericComparisonRoundingMm: 1e-9,
}
fs.writeFileSync(`${dir}/regression-summary.json`, JSON.stringify(report, null, 2) + "\n")
console.log(JSON.stringify({ ...report, inputs: undefined, existingPlacementChanges: report.existingPlacementChanges.map(e => e.name) }, null, 2))