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))