AnasSarkiz/muse-voice-remote

Muse Voice Remote — bare ESP32-S3R8, 155 TOP JLCPCB parts with real supplier CAD, native 3D assembly and GLB exports. Revised 17 mm case allocation; 0.30 mm via drill /0.45 mm pad. Checked prototype PCB; mechanical, RF and MUSE qualification pending.

Version
1.3.2
License
unset
Stars
0

scripts/audit-jlcpcb.py

#!/usr/bin/env python3
"""Audit the current BOM against JLCPCB's live assembly catalog.

Text search in a third-party index is insufficient proof: compare exact MPNs
and retain the official response, including quantity and manufacturer.
"""
import concurrent.futures
import csv
import datetime
import hashlib
import json
import pathlib
import subprocess

ROOT = pathlib.Path(__file__).resolve().parents[1]
ENDPOINT = "https://jlcpcb.com/api/overseas-pcb-order/v1/shoppingCart/smtGood/selectSmtComponentList/v2"
OUT = ROOT / "docs/jlcpcb-audit"
OUT.mkdir(parents=True, exist_ok=True)


def audit(row):
    mpn = row["MPN"]
    query = {"currentPage": 1, "pageSize": 100, "keyword": mpn,
             "searchSource": "search", "searchType": 2,
             "componentBrandList": [], "componentSpecificationList": [],
             "componentAttributeList": [], "paramList": []}
    response = subprocess.run(
        ["curl", "--silent", "--show-error", "--fail-with-body", "--max-time", "30",
         "-H", "Content-Type: application/json", "--data-binary", json.dumps(query), ENDPOINT],
        capture_output=True, text=True,
    )
    checked = datetime.datetime.now(datetime.timezone.utc).isoformat()
    filename = "".join(c if c.isalnum() or c in "-_." else "_" for c in mpn) + ".json"
    raw = response.stdout
    result = {"mpn": mpn, "quantity": int(row["Quantity"]),
              "references": row["References"].split(), "checked_at": checked,
              "source": ENDPOINT, "raw_response": "docs/jlcpcb-audit/" + filename,
              "raw_sha256": hashlib.sha256(raw.encode()).hexdigest(), "exact_matches": []}
    def retain_failed_lookup():
        # Never replace a verified catalog snapshot with a proxy error page.
        # Keep the failed attempt separately and make its failed status clear.
        previous = OUT / filename
        if previous.exists():
            result["retained_snapshot"] = {"file": str(previous.relative_to(ROOT)),
                "sha256": hashlib.sha256(previous.read_bytes()).hexdigest()}
        failed = OUT / (filename.removesuffix(".json") + ".failed-response.txt")
        failed.write_text(raw)
        result["raw_response"] = str(failed.relative_to(ROOT))
    if response.returncode:
        result["status"] = "lookup_failed"
        result["error"] = response.stderr.strip() or raw[:250]
        retain_failed_lookup()
        return result
    try:
        data = json.loads(raw)
        info = data["data"]["componentPageInfo"]
        if info.get("total") == 0 and info.get("list") is None:
            info["list"] = []
        if data["code"] != 200 or not isinstance(info["list"], list):
            raise ValueError("Unexpected JLCPCB catalog response")
        (OUT / filename).write_text(raw)
        result["total_search_matches"] = info["total"]
        for part in info["list"]:
            if str(part.get("componentModelEn", "")).strip().upper() != mpn.upper():
                continue
            result["exact_matches"].append({
                "lcsc": part["componentCode"], "mpn": part["componentModelEn"],
                "manufacturer": part.get("componentBrandEn"),
                "package": part.get("componentSpecificationEn"),
                "stock": part.get("stockCount"),
                "library_type": part.get("componentLibraryType"),
                "datasheet_url": part.get("dataManualOfficialLink") or part.get("dataManualUrl"),
                "assembly_component": part.get("assemblyComponentFlag"),
            })
        stocked = [p for p in result["exact_matches"]
                   if isinstance(p["stock"], int) and p["stock"] >= result["quantity"]]
        result["status"] = "stocked_exact_candidate" if stocked else "no_stocked_exact_match"
    except (KeyError, TypeError, ValueError) as error:
        result["status"] = "lookup_failed"
        result["error"] = str(error)
        retain_failed_lookup()
    return result


with (ROOT / "docs/procurement.csv").open() as fd:
    bom = list(csv.DictReader(fd))
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
    results = list(executor.map(audit, bom))
report = {"source": "Official JLCPCB live assembly catalog",
          "scope": "Exact current BOM candidates; not replacement approval or import validation",
          "parts": results}
(ROOT / "docs/jlcpcb-stock-audit.json").write_text(json.dumps(report, indent=2) + "\n")
for row in results:
    matches = ", ".join(f"{p['lcsc']} ({p['manufacturer']}, stock {p['stock']})"
                        for p in row["exact_matches"][:3])
    print(row["mpn"], row["status"], matches, flush=True)
print("Exact stocked MPNs:", sum(r["status"] == "stocked_exact_candidate" for r in results), "/", len(results))