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