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