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