imrishabh18/rp2040-motor-controller
This circuit incorporates a USB-C Power Delivery (PD) interface with a CH224K controller, a WJ500V-5.08-04P motor output connector, multiple power conditioning components including capacitors, resistors, diodes, and an RP2040 microcontroller, to manage motor control, signal routing, and power management for a USB-powered stepper motor system.
- Version
- 1.0.14
- License
- unset
- Stars
- 0
scripts/audit-jlcpcb-stock.mjs
import { createHash } from "node:crypto";
import { mkdir } from "node:fs/promises";
import { resolve } from "node:path";
// Read public, server-rendered product data only. Never execute page scripts.
export function parseJlcProduct(html, code) {
const stream = [...html.matchAll(/self\.__next_f\.push\((.*?)\)<\/script>/gs)]
.map((match) => {
const chunk = JSON.parse(match[1]);
return chunk[0] === 1 && typeof chunk[1] === "string" ? chunk[1] : "";
})
.join("");
for (const line of stream.split("\n")) {
const record = line.match(/^[0-9a-f]+:(\{.*\})$/);
if (!record) continue;
let product;
try {
product = JSON.parse(record[1]);
} catch {
continue;
}
if (
product.componentCode === code &&
typeof product.overseasStockCount === "number" &&
typeof product.canPresaleNumber === "number" &&
typeof product.isBuyComponent === "string"
) {
return {
manufacturerPartNumber: product.componentModelEn,
manufacturer: product.componentBrandEn,
package: product.componentSpecificationEn,
// These are the fields the JLCPCB product-page UI uses for
// stockCount and maxInStock respectively. Preserve both; do not
// confuse them with LCSC retail stock or reserved private stock.
displayedStock: product.overseasStockCount,
orderableStock: product.canPresaleNumber,
canBuy: product.isBuyComponent !== "0",
minimumOrder: product.minPurchaseNum ?? null,
preorderMinimum: product.preMinPurchaseNum ?? null,
assemblyLossQuantity: product.lossNumber ?? null,
minimumAssemblyQuantity: product.leastPatchNumber ?? null,
};
}
}
throw new Error(`No unambiguous JLCPCB stock record found for ${code}`);
}
async function getStock(code) {
const url = `https://jlcpcb.com/partdetail/${code}`;
const proc = Bun.spawn(
["curl", "--fail", "--location", "--silent", "--show-error", "--max-time", "30", url],
{ stdout: "pipe", stderr: "pipe" },
);
const [html, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
if (exitCode !== 0) throw new Error(stderr.trim() || `curl exited ${exitCode}`);
return { ...parseJlcProduct(html, code), checkedAt: new Date().toISOString(), url };
}
async function main() {
const circuitPath = resolve(process.argv[2] ?? "dist/index/circuit.json");
const outputDir = resolve(process.argv[3] ?? "reports");
const raw = await Bun.file(circuitPath).text();
const circuit = JSON.parse(raw);
const components = circuit.filter((e) => e.type === "source_component");
const excluded = components.filter((e) => e.ftype === "simple_test_point").map((e) => e.name);
const populated = components.filter((e) => e.ftype !== "simple_test_point");
const missing = populated.filter((e) => !/^C\d+$/.test(e.supplier_part_numbers?.jlcpcb?.[0] ?? ""));
const grouped = new Map();
for (const component of populated) {
const code = component.supplier_part_numbers?.jlcpcb?.[0];
if (!/^C\d+$/.test(code ?? "")) continue;
if (!grouped.has(code)) grouped.set(code, []);
grouped.get(code).push(component.name);
}
const queue = [...grouped].sort(([a], [b]) => Number(a.slice(1)) - Number(b.slice(1)));
const results = [];
let next = 0;
await Promise.all(Array.from({ length: 3 }, async () => {
while (next < queue.length) {
const [code, designators] = queue[next++];
const base = { code, designators, quantityPerBoard: designators.length };
try {
const stock = await getStock(code);
const status = !stock.canBuy ? "not-buyable" :
stock.displayedStock <= 0 || stock.orderableStock < designators.length ? "shortage" : "in-stock";
results.push({ ...base, ...stock, status });
console.log(`${code} ${designators.join(",")} ${status}: displayed=${stock.displayedStock}, orderable=${stock.orderableStock}`);
} catch (error) {
results.push({ ...base, status: "unverified", error: String(error), url: `https://jlcpcb.com/partdetail/${code}` });
console.log(`${code}: UNVERIFIED ${error}`);
}
}
}));
results.sort((a, b) => Number(a.code.slice(1)) - Number(b.code.slice(1)));
const report = {
checkedAt: new Date().toISOString(),
circuitPath,
circuitSha256: createHash("sha256").update(raw).digest("hex"),
populatedComponentCount: populated.length,
uniquePartCount: grouped.size,
missingCodes: missing.map((e) => e.name),
excludedBareTestPads: excluded,
caveats: [
"Public JLCPCB part-page stock; not LCSC retail or reserved private inventory.",
"Stock is not reserved and can change. Assembly quantity, attrition, minimums and checkout availability must be confirmed for the actual order.",
"In-stock only establishes availability for the per-board quantity before assembly attrition, not electrical suitability or fabrication readiness.",
"Existing fabrication ZIP is stale. This audit does not update Gerbers or approve the design.",
],
parts: results,
};
const csvRow = (cells) => cells.map((v) => `"${String(v ?? "").replaceAll('"', '""')}"`).join(",");
const csv = [
csvRow(["Designators", "Quantity Per Board", "JLCPCB Part #", "Manufacturer Part Number", "Package", "Displayed Stock", "Orderable Stock", "Status", "Checked At UTC", "Source"]),
...results.map((r) => csvRow([r.designators.join(", "), r.quantityPerBoard, r.code, r.manufacturerPartNumber, r.package, r.displayedStock, r.orderableStock, r.status, r.checkedAt, r.url])),
].join("\n") + "\n";
await mkdir(outputDir, { recursive: true });
await Bun.write(resolve(outputDir, "jlcpcb-stock-audit.json"), JSON.stringify(report, null, 2) + "\n");
await Bun.write(resolve(outputDir, "jlcpcb-stock-audit.csv"), csv);
console.log(JSON.stringify({ populated: populated.length, unique: grouped.size, excluded, missing: report.missingCodes, issues: results.filter((r) => r.status !== "in-stock") }, null, 2));
if (missing.length || results.some((r) => r.status !== "in-stock")) process.exitCode = 1;
}
if (import.meta.main) await main();