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
design/validate-catalog.ts
import { readFileSync } from "node:fs"
import type { Part } from "./parts"
type Selection = { parts: { lcsc: string; mpn: string; quantity: number; references: string[]; raw_response: string }[] }
export function electricalValue(value: string): number {
const match = value.replace(/[ΩΩ\s]/g, "").replace(/[µμ]/g, "u")
.match(/^(\d+(?:\.\d+)?)([pnumkM]?)(?:F|H)?$/)
if (!match) throw new Error(`Unsupported electrical value ${value}`)
return Number(match[1]) * ({ p: 1e-12, n: 1e-9, u: 1e-6, m: 1e-3, "": 1, k: 1e3, M: 1e6 }[match[2]]!)
}
// A two-terminal device can pass connectivity while being the wrong part.
// Check supplier identity, family and value independently of pad/net geometry.
export function validateCatalog(parts: Part[], selection: Selection) {
const failures: string[] = []
const rows: { reference: string; lcsc: string; mpn: string; kind: string; value?: string; supplierValue?: string; supplierCategory?: string; ratedVoltage?: string }[] = []
for (const entry of selection.parts) {
if (entry.quantity !== entry.references.length) failures.push(`${entry.lcsc}: quantity/reference mismatch`)
if (new Set(entry.references).size !== entry.references.length) failures.push(`${entry.lcsc}: duplicate references`)
}
for (const part of parts) {
const matches = selection.parts.filter(entry => entry.references.includes(part.name))
if (matches.length !== 1) { failures.push(`${part.name}: expected exactly one supplier assignment, found ${matches.length}`); continue }
const entry = matches[0]
const raw = JSON.parse(readFileSync(`vendor/jlcpcb/${entry.lcsc}.raweasy.json`, "utf8"))
const supplier = raw.dataStr.head.c_para
const official = JSON.parse(readFileSync(entry.raw_response, "utf8"))
const records = official.data?.componentPageInfo?.list ?? []
const identity = records.find((record: { componentCode: string; componentModelEn: string }) =>
record.componentCode === entry.lcsc && record.componentModelEn.trim().toUpperCase() === entry.mpn.toUpperCase())
if (!identity) failures.push(`${part.name}: official supplier identity mismatch`)
if ((raw.lcsc?.number ?? raw.szlcsc?.number) !== entry.lcsc) failures.push(`${part.name}: imported C-number mismatch`)
const family = `${(raw.tags ?? []).join(" ")} ${identity?.componentTypeEn ?? ""}`
const expected = part.kind ?? (/^C_/.test(part.name) ? "capacitor" : /^R_/.test(part.name) ? "resistor" : /^L_/.test(part.name) ? "inductor" : undefined)
const familyPatterns = { capacitor: /capacitor|MLCC/i, resistor: /resistor/i, inductor: /inductor|coil/i, led: /light emitting|LED/i, diode: /diode|Schottky|ESD protection/i }
if (expected && !familyPatterns[expected].test(family)) failures.push(`${part.name}: expected ${expected}, supplier is ${family}`)
const deviceFamily = part.name === "U1" ? /RF Transceiver|Microcontroller/i
: part.name === "U_FLASH" ? /NOR FLASH/i
: part.name === "U_CHARGE" ? /Battery Management/i
: part.name === "U_REG" ? /DC-DC/i
: part.name === "U_AMP" ? /Audio.*(Amp|OpAmp)/i
: /^U_(LCD|MOTOR)_POWER$/.test(part.name) ? /Linear Voltage|LDO/i
: part.name === "U_BACKLIGHT" ? /LED Driver/i
: /^U_(ISO|LCD_BUF)/.test(part.name) ? /Buffer|Driver/i
: /^MIC[12]$/.test(part.name) ? /MEMS Microphone/i
: part.name === "Y1" ? /Crystal/i
: /^Q_/.test(part.name) ? (["Q_BAT_EN","Q_BL_DRIVE"].includes(part.name) ? /Bipolar|BJT/i : /MOSFET/i)
: part.name === "F_USB" ? /Resettable Fuse/i : undefined
if (deviceFamily && !deviceFamily.test(family)) failures.push(`${part.name}: incompatible supplier device family ${family}`)
if (expected && ["resistor", "capacitor", "inductor"].includes(expected)) {
try {
const intended = electricalValue(part.value), actual = electricalValue(supplier.Value ?? "")
if (Math.abs(intended - actual) > Math.max(Math.abs(intended), Math.abs(actual), 1e-15) * 1e-9)
failures.push(`${part.name}: expected ${part.value}, supplier value ${supplier.Value}`)
} catch (error) { failures.push(`${part.name}: ${String(error)}`) }
}
const ratedVoltage = identity?.attributes?.find((attribute: { attribute_name_en: string }) => attribute.attribute_name_en === "Voltage Rating")?.attribute_value_name
if (expected === "capacitor") {
const volts = Number(String(ratedVoltage ?? "").replace(/V$/i, ""))
const required = part.name === "C_BL_OUT" ? 50 : Object.values(part.nets).some(net => /USB|VBUS/.test(net)) ? 10 : 6.3
if (!Number.isFinite(volts) || volts < required) failures.push(`${part.name}: capacitor rating ${ratedVoltage ?? "missing"} is below ${required} V design requirement`)
}
rows.push({ reference: part.name, lcsc: entry.lcsc, mpn: entry.mpn, kind: expected ?? "device", value: expected ? part.value : undefined, supplierValue: supplier.Value, supplierCategory: identity?.componentTypeEn, ratedVoltage })
}
for (const entry of selection.parts) for (const ref of entry.references)
if (!parts.some(part => part.name === ref)) failures.push(`${entry.lcsc}: unknown reference ${ref}`)
return { checkedComponents: rows.length, failures, rows }
}