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/import-jlcpcb.ts

import { mkdir, readFile, writeFile } from "node:fs/promises"
import { createHash } from "node:crypto"
import { EasyEdaJsonSchema, convertEasyEdaJsonToCircuitJson, convertRawEasyEdaToTs, fetchEasyEDAComponent } from "easyeda"
import { preserveSupplierCad } from "./preserve-jlcpcb-cad"
import { preserveThermalVias } from "./preserve-jlcpcb-vias"

const selection = JSON.parse(await readFile("docs/jlcpcb-import-selection.json", "utf8")) as {
  parts: { lcsc: string; mpn: string; references: string[] }[]
}
await mkdir("imports", { recursive: true })
await mkdir("vendor/jlcpcb", { recursive: true })
const results: Record<string, unknown>[] = []
async function importPart(part: typeof selection.parts[number]) {
  const source = `https://jlcsearch.tscircuit.com/api/easyeda_components/${part.lcsc}`
  const rawFile = `vendor/jlcpcb/${part.lcsc}.raweasy.json`
  let raw: unknown
  try { raw = JSON.parse(await readFile(rawFile, "utf8")) } catch {
    const response = await fetch(source)
    if (response.ok) {
      const data = await response.json() as { easyeda_component_details?: { easyeda_json?: unknown } }
      raw = data.easyeda_component_details?.easyeda_json
    }
    if (!raw) raw = await fetchEasyEDAComponent(part.lcsc, { includeModelMetadata: false })
    await writeFile(rawFile, JSON.stringify(raw, null, 2) + "\n")
  }
  // EasyEDA has two pin numbers: SPICE order and displayed physical number.
  // Some symbols (notably ESP32) repeat SPICE order on each side. Normalize
  // symbol metadata to the physical display number before the stock converter;
  // retain the original response and leave packageDetail geometry untouched.
  const normalized = JSON.parse(JSON.stringify(raw))
  const symbol = typeof normalized.dataStr === "string" ? JSON.parse(normalized.dataStr) : normalized.dataStr
  const symbolPinNumberCorrections: { spice: string; physical: string }[] = []
  symbol.shape = symbol.shape.map((shape: string) => {
    if (!shape.startsWith("P~")) return shape
    const segments = shape.split("^^")
    const physical = segments[4]?.split("~")[4]
    const settings = segments[0].split("~")
    if (physical && /^\d+$/.test(physical) && settings[3] !== physical) {
      symbolPinNumberCorrections.push({ spice: settings[3], physical })
      settings[3] = physical
      segments[0] = settings.join("~")
    }
    return segments.join("^^")
  })
  normalized.dataStr = symbol
  const parsed = EasyEdaJsonSchema.parse(normalized)
  const mpn = parsed.dataStr.head.c_para["Manufacturer Part"]
  if (mpn?.toUpperCase() !== part.mpn.toUpperCase() || parsed.lcsc.number !== part.lcsc) {
    throw new Error(`JLCPCB import mismatch: expected ${part.lcsc}/${part.mpn}, received ${parsed.lcsc.number}/${mpn}`)
  }
  const generatedTsx = await convertRawEasyEdaToTs({ rawEasy: normalized })
  const geometry = preserveThermalVias(generatedTsx,
    convertEasyEdaJsonToCircuitJson(parsed, { shouldRecenter: true, useModelCdn: true }))
  const circuit = geometry.circuit
  const cad = circuit.find(e => e.type === "cad_component")
  const preserved = preserveSupplierCad(geometry.tsx, cad)
  const tsx = preserved.tsx
  const name = tsx.match(/export const (\w+)\s*=/)?.[1]
  if (!name || !tsx.includes(part.lcsc)) throw new Error(`Incomplete imported component ${part.lcsc}`)
  const path = `imports/${part.lcsc}.tsx`
  await writeFile(path, tsx + "\n")
  await writeFile(`vendor/jlcpcb/${part.lcsc}.circuit.json`, JSON.stringify(circuit, null, 2) + "\n")
  const rawBytes = await readFile(rawFile)
  results.push({ lcsc: part.lcsc, mpn, componentName: name, file: path,
    sha256: createHash("sha256").update(tsx + "\n").digest("hex"),
    rawFile, rawSha256: createHash("sha256").update(rawBytes).digest("hex"),
    source, easyedaUuid: parsed.uuid, converter: "easyeda@0.0.372", symbolPinNumberCorrections,
    cad: { objUrl: cad?.model_obj_url, stepUrl: cad?.model_step_url, placement: preserved.placement },
    thermalViaPinBindings: geometry.bindings,
    footprintMode: "exact imported EasyEDA geometry; no footprinter substitution",
    references: part.references })
  console.log(`Imported ${part.lcsc} ${mpn}: ${name}`)
}
let cursor = 0
const errors: string[] = []
async function worker() {
  while (cursor < selection.parts.length) {
    const part = selection.parts[cursor++]
    try { await importPart(part) } catch (error) { errors.push(`${part.lcsc}: ${String(error)}`); console.error(errors.at(-1)) }
  }
}
await Promise.all([worker(), worker()])
results.sort((a, b) => String(a.lcsc).localeCompare(String(b.lcsc)))
await writeFile("vendor/jlcpcb/import-manifest.json", JSON.stringify({ imports: results, errors }, null, 2) + "\n")
if (!errors.length) {
  await writeFile("imports/index.ts", 'import type { ComponentType } from "react"\n' +
    results.map(entry=>`import { ${entry.componentName} as ${entry.lcsc} } from "./${entry.lcsc}"`).join("\n") +
    '\n\nexport const jlcComponents: Record<string, ComponentType<any>> = {\n' +
    results.map(entry=>`  ${entry.lcsc},`).join("\n") + '\n}\n')
}
if (errors.length) process.exitCode = 1
console.log(`JLCPCB imports: ${results.length}/${selection.parts.length}; errors: ${errors.length}`)