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/publish-software-current.mjs

import fs from "node:fs"
import path from "node:path"
import os from "node:os"
import crypto from "node:crypto"
import assert from "node:assert/strict"
import { gzipSync } from "node:zlib"

// Use the same release/archive endpoints as tsci push. Keep the previous
// latest until the frozen sources and completed website build are verified.
const packageName = "shibosoftwaredev/f1c100s-linux-nema17-stepper-controller"
const version = "0.4.56"
const action = process.argv[2] ?? "status"
const manifestPath = "reports/software-current-publish-manifest.json"
const receiptPath = "reports/software-current-publication.json"
const read = p => JSON.parse(fs.readFileSync(p, "utf8"))
const sha = b => crypto.createHash("sha256").update(b).digest("hex")
const hash = p => sha(fs.readFileSync(p))
// Preserve small electrical quantities as well as PCB geometry.
const stable = value => JSON.stringify(value, (_, item) =>
  typeof item === "number" ? (Math.abs(item) < 1e-4 ? Number(item.toPrecision(12)) : Math.round(item * 1e8) / 1e8) :
    item && typeof item === "object" && !Array.isArray(item) ?
      Object.fromEntries(Object.entries(item).sort(([a], [b]) => a.localeCompare(b))) : item)
const records = c => c.filter(e => e.type !== "source_project_metadata").map(stable).sort()
const recordsHash = c => sha(JSON.stringify(records(c)))
const write = (p, value) => fs.writeFileSync(p, JSON.stringify(value, null, 2) + "\n")

async function request(endpoint, options = {}) {
  const { body, write: authenticated = false, missing = false, timeout = 45000 } = options
  const token = authenticated ? (process.env.TSCIRCUIT_TOKEN ??
    read(path.join(os.homedir(), "Library/Preferences/tscircuit-nodejs/config.json")).sessionToken) : null
  const r = await fetch("https://registry-api.tscircuit.com/" + endpoint, {
    method: body ? "POST" : "GET",
    headers: { ...(body ? { "content-type": "application/json" } : {}),
      ...(token ? { authorization: `Bearer ${token}` } : {}) },
    body: body ? JSON.stringify(body) : undefined,
    signal: AbortSignal.timeout(timeout),
  })
  if (missing && r.status === 404) return null
  assert.ok(r.ok, endpoint.split("?")[0] + " HTTP " + r.status)
  return r.json()
}
const getPackage = () => request("packages/get", { body: { name: packageName } }).then(r => r.package)
const getRelease = () => request("package_releases/get", {
  body: { package_name_with_version: `${packageName}@${version}` }, missing: true,
}).then(r => r?.package_release ?? null)
const list = id => request("package_files/list?" + new URLSearchParams({ package_release_id: id })).then(r => r.package_files)
async function bytes(entry) {
  assert.ok(entry, "required remote file exists")
  const e = (await request("package_files/get?" + new URLSearchParams({ package_file_id: entry.package_file_id }))).package_file
  if (e.content_text != null) return Buffer.from(e.content_text)
  if (e.content_base64 != null) return Buffer.from(e.content_base64, "base64")
  const r = await fetch("https://registry-api.tscircuit.com/package_files/download?" +
    new URLSearchParams({ package_file_id: entry.package_file_id }), { signal: AbortSignal.timeout(45000) })
  assert.ok(r.ok, "binary download HTTP " + r.status)
  return Buffer.from(await r.arrayBuffer())
}
function validateLocal() {
  const a = read("reports/software-current-acceptance.json")
  assert.equal(a.status, "LOCAL_VALIDATION_PASS_NOT_FABRICATION_APPROVAL")
  assert.equal(a.validationStages, 24)
  assert.equal(a.nativeErrors, 0)
  assert.equal(a.fabricationReady, false)
  assert.equal(hash("dist/index/circuit.json"), a.circuitSha256)
  for (const [file, digest] of Object.entries(a.evidence)) assert.equal(hash(file), digest, file)
  const checkpoint = read("../../checkpoints/nema17-software-current-validated-20261024-Og11v7/checkpoint-manifest.json")
  assert.equal(checkpoint.restoreVerified, true)
  assert.equal(hash(checkpoint.archive), checkpoint.archiveSha256)
  for (const f of checkpoint.files) assert.equal(hash(f.file), f.sha256, `checkpoint unchanged: ${f.file}`)
  return a
}

if (action === "prepare") {
  const a = validateLocal()
  assert.ok(!fs.existsSync(manifestPath), "Never replace a frozen publication manifest")
  const previous = await getPackage()
  assert.equal(previous.latest_version, "0.4.55")
  assert.equal(previous.latest_package_release_id, "794faf39-dd13-46b5-ab06-a334d1b51f81")
  assert.equal(await getRelease(), null, "Never overwrite a numbered release")
  const baselineBytes = await bytes((await list(previous.latest_package_release_id)).find(e => e.file_path === "dist/index/circuit.json"))
  assert.equal(recordsHash(JSON.parse(baselineBytes)), recordsHash(read("baseline/v055-circuit.json")),
    "Live latest must still match the audited comparison baseline")
  const comparison = { checkedAt: new Date().toISOString(), previousVersion: previous.latest_version,
    previousReleaseId: previous.latest_package_release_id, previousCircuitSha256: sha(baselineBytes),
    candidateCircuitSha256: a.circuitSha256, liveBaselineMatchesFrozenBaseline: true,
    noUnintendedRegressionsInCheckedScope: true, warnings: a.warnings, feature: a.feature,
    fabricationReady: false, qualificationStillRequired: a.qualificationStillRequired }
  write("reports/software-current-prepush-comparison.json", comparison)
  const selected = new Set(read("reports/clean-schematic-publish-manifest.json").files.map(f => f.file))
  const visit = dir => {
    for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
      if (["build", "__pycache__", "node_modules", ".git"].includes(e.name) || e.name.startsWith(".")) continue
      const p = path.join(dir, e.name)
      if (e.isDirectory()) visit(p)
      else { assert.ok(e.isFile(), `unexpected link: ${p}`); selected.add(p) }
    }
  }
  for (const dir of ["imports", "footprints", "modules", "firmware", "tests", "scripts", "routing", "baseline"]) visit(dir)
  for (const e of fs.readdirSync(".", { withFileTypes: true }))
    if (e.isFile() && !e.name.startsWith(".") && /\.(tsx?|json|md)$/.test(e.name)) selected.add(e.name)
  for (const file of ["bun.lock", "requirements-routing.txt", "dist/index/circuit.json", "dist/index/pcb.svg", "dist/index/schematic.svg", "reports/software-current-acceptance.json",
    "reports/software-current-prepush-comparison.json", ...Object.keys(a.evidence)]) selected.add(file)
  const destination = fs.mkdtempSync(path.resolve("../../publish-staging/nema17-software-current-v056-"))
  for (const file of selected) {
    assert.ok(!file.includes("..") && !/(?:^|\/)(?:node_modules|\.env|\.npmrc|\.netrc)(?:\/|$)/.test(file), file)
    const target = path.join(destination, file)
    fs.mkdirSync(path.dirname(target), { recursive: true })
    fs.copyFileSync(file, target)
  }
  const metadata = read("package.json"); metadata.version = version
  write(path.join(destination, "package.json"), metadata)
  const lock = read("package-lock.json"); lock.version = version
  if (lock.packages?.[""]) lock.packages[""].version = version
  write(path.join(destination, "package-lock.json"), lock)
  const readme = fs.readFileSync("README.md", "utf8")
    .replace(/> RELEASE 0\.4\.55:[\s\S]*?\n\n/, `> RELEASE ${version}: software-adjustable current, based on published 0.4.55.\n> Nominal common peak current: 0–396 mA/phase, configurable while disarmed.\n> New PCB and matching MCU firmware must be used together; R_ISET_210 stays DNP.\n> The 42.3 mm four-layer outline, mounts, USB locations, top bulk capacitor and\n> motor/cable assembly are unchanged. 155 warnings remain visible: 151 inherited\n> plus four audited whole-net width diagnostics; dedicated new routes are 0.15 mm.\n> Not fabrication-qualified. Frozen pre-publication evidence retains its original\n> local/unpublished labels; registry publication and the publication receipt\n> establish the uploaded version.\n\n`)
    .replace("### Release 0.4.55: clean schematic sheets", `### Release ${version}: software-adjustable current\n\nMCP4725 on the existing motion-MCU I2C bus supplies the DRV8825 reference.\nBoot zero/readback, disarmed-only configuration, fault shutdown and settling\ninterlocks are implemented and tested. No EEPROM writes are used. Reset requests\n396 mA; zero cannot arm. See firmware/PROTOCOL.md for the new commands.\nThis does not extend the motor-current ceiling or qualify a 1.5 A NEMA17.\nAll 24 validation stages pass; 29 board and 32 module tests pass. Read\nreports/software-current-acceptance.json and verification.json for qualification\ngaps, including bench calibration and actual rear-motor fastening.\n\n### Historical release 0.4.55: clean schematic sheets`)
  fs.writeFileSync(path.join(destination, "README.md"), readme)
  const files = [...selected].sort().map(file => ({ file, sha256: hash(path.join(destination, file)),
    bytes: fs.statSync(path.join(destination, file)).size }))
  const m = { createdAt: new Date().toISOString(), source: process.cwd(), destination, packageName, version,
    previous: { version: previous.latest_version, releaseId: previous.latest_package_release_id },
    circuitSha256: a.circuitSha256, fabricationReady: false,
    stagingOnlyMetadataChanges: ["package.json", "package-lock.json", "README.md"], files }
  write(manifestPath, m)
  console.log(JSON.stringify({ version, destination, files: files.length, bytes: files.reduce((n, f) => n + f.bytes, 0) }))
  process.exit(0)
}

const m = read(manifestPath)
let p = await getPackage(), r = await getRelease()
assert.ok([m.previous.releaseId, r?.package_release_id].includes(p.latest_package_release_id), "Unrelated latest change: stop")
if (action === "upload") {
  validateLocal()
  assert.equal(recordsHash(read(path.join(m.destination, "dist/index/circuit.json"))), recordsHash(read("dist/index/circuit.json")))
  if (!r) {
    await request("package_releases/create", { write: true, body: { package_name_with_version: `${packageName}@${version}` } })
    r = await getRelease()
  }
  p = await getPackage()
  if (p.latest_package_release_id !== m.previous.releaseId)
    await request("package_releases/update", { write: true, body: { package_release_id: m.previous.releaseId, is_latest: true } })
  const payload = m.files.map(f => {
    const b = fs.readFileSync(path.join(m.destination, f.file)); assert.equal(sha(b), f.sha256, f.file)
    const text = !b.includes(0) && Buffer.from(b.toString("utf8")).equals(b)
    return text ? { file_path: f.file, content_text: b.toString("utf8") } : { file_path: f.file, content_base64: b.toString("base64") }
  })
  const batches = []
  const split = files => {
    const archive = gzipSync(JSON.stringify({ files }), { level: 9 })
    if (archive.length > 2_000_000) {
      assert.ok(files.length > 1, "single file too large")
      const half = Math.ceil(files.length / 2); split(files.slice(0, half)); split(files.slice(half))
    } else batches.push({ archive, count: files.length })
  }
  split(payload)
  for (const [index, batch] of batches.entries()) {
    console.log(JSON.stringify({ uploading: true, releaseId: r.package_release_id, batch: index + 1,
      batches: batches.length, files: batch.count, gzipBytes: batch.archive.length }))
    await request("package_files/upload_archive", { write: true, timeout: 240000,
      body: { package_name_with_version: `${packageName}@${version}`, archive_base64: batch.archive.toString("base64") } })
  }
  console.log(JSON.stringify({ uploaded: true, releaseId: r.package_release_id }))
  process.exit(0)
}
assert.ok(r, "release exists")
if (["ready", "verify", "promote"].includes(action)) {
  const entries = new Map((await list(r.package_release_id)).map(e => [e.file_path, e])), pending = [...m.files]
  await Promise.all(Array.from({ length: 6 }, async () => {
    while (pending.length) {
      const f = pending.shift()
      if (f.file.startsWith("dist/")) continue // build outputs are verified separately below
      assert.equal(sha(await bytes(entries.get(f.file))), f.sha256, `uploaded source: ${f.file}`)
    }
  }))
  write("reports/software-current-source-upload-verification.json", { checkedAt: new Date().toISOString(),
    version, releaseId: r.package_release_id, files: m.files.filter(f => !f.file.startsWith("dist/")), allBytesMatch: true })
  if (action === "ready") await request("package_releases/update", { write: true,
    body: { package_release_id: r.package_release_id, ready_to_build: true } })
}
assert.ok(["status", "ready", "verify", "promote"].includes(action), "known action")
r = await getRelease(); p = await getPackage()
const b = r.latest_package_build_id ? (await request("package_builds/get?" +
  new URLSearchParams({ package_build_id: r.latest_package_build_id }))).package_build : null
const errors = b ? [b.user_code_job_error, b.circuit_json_build_error, b.transpilation_error,
  b.image_generation_error, b.user_code_job_infrastructure_error].filter(Boolean) : []
if (b) write("reports/software-current-cloud-build-status.json", b)
const report = { checkedAt: new Date().toISOString(), version, releaseId: r.package_release_id,
  buildId: r.latest_package_build_id, buildStartedAt: b?.user_code_job_started_at,
  buildCompletedAt: b?.user_code_job_completed_at, buildInProgress: b?.build_in_progress,
  errors, currentLatest: p.latest_version, latestVerified: p.latest_package_release_id === r.package_release_id,
  fabricationReady: false }
if (action === "verify" || action === "promote") {
  assert.ok(b?.user_code_job_completed_at && !b.build_in_progress && !errors.length, "Completed successful website build required")
  const entries = new Map((await list(r.package_release_id)).map(e => [e.file_path, e]))
  for (const file of ["dist/index/circuit.json", "dist/index/pcb.svg", "dist/index/schematic.svg", "dist/index/3d.png", "dist/index.html"])
    assert.ok(entries.has(file), file)
  const cloudBytes = await bytes(entries.get("dist/index/circuit.json")), cloud = JSON.parse(cloudBytes)
  assert.equal(hash("dist/index/circuit.json"), m.circuitSha256)
  assert.equal(recordsHash(cloud), recordsHash(read("dist/index/circuit.json")),
    "Every cloud design/diagnostic record matches audited local circuit (excluding project filesystem metadata only)")
  assert.equal(cloud.filter(e => e.type.endsWith("_error")).length, 0)
  assert.equal(cloud.filter(e => e.type.endsWith("_warning")).length, 155)
  fs.writeFileSync("reports/software-current-website-circuit.json", cloudBytes)
  Object.assign(report, { sourceFilesVerified: m.files.filter(f => !f.file.startsWith("dist/")).length,
    cloudMatchesAuditedBoard: true, localCircuitSha256: m.circuitSha256,
    cloudCircuitSha256: sha(cloudBytes), nativeErrors: 0, nativeWarnings: 155 })
  if (action === "promote") {
    p = await getPackage()
    assert.ok([m.previous.releaseId, r.package_release_id].includes(p.latest_package_release_id), "No concurrent unrelated latest")
    if (p.latest_package_release_id !== r.package_release_id)
      await request("package_releases/update", { write: true, body: { package_release_id: r.package_release_id, is_latest: true } })
    p = await getPackage(); assert.equal(p.latest_package_release_id, r.package_release_id)
    report.latestVerified = true; report.currentLatest = p.latest_version
  }
  write(receiptPath, report)
}
console.log(JSON.stringify(report))