ShiboSoftwareDev/MSP-EXP430FR2433

This code defines a 4 MHz surface-mount quartz crystal resonator with 15pF load capacitance and ground pin configuration, including PCB footprint and 3D model details.

Version
1.0.1
License
unset
Stars
0

scripts/generate-assembly-files.mjs

import { mkdirSync, readFileSync, writeFileSync } from "node:fs"
import { dirname, resolve } from "node:path"

const inputPath = resolve(process.argv[2] ?? "dist/index/circuit.json")
const outputDir = resolve(process.argv[3] ?? "fabrication")
const circuit = JSON.parse(readFileSync(inputPath, "utf8"))

const sourceComponents = new Map(
  circuit
    .filter((element) => element.type === "source_component")
    .map((component) => [component.source_component_id, component]),
)
const pcbComponents = circuit.filter(
  (element) => element.type === "pcb_component",
)

const footprintByReference = {
  J_USB: "Micro-USB-B MicroXNJ",
  U_ESD: "SOT-23-6",
  U_LDO: "SOT-23-5",
  U_DBG: "VQFN-64 RGC (9x9mm + exposed pad)",
  Y_DBG: "Murata CSTCR 3-pin resonator",
  J_DBG_PROG: "P2.54mm 1x7 THT",
  U_TARGET: "VQFN-24 RGE (4x4mm + exposed pad)",
  SW_RESET: "TS342A2P-WZ",
  SW1: "TS342A2P-WZ",
  SW2: "TS342A2P-WZ",
  Y_TARGET: "SC-32S 32.768kHz crystal",
  J1: "P2.54mm 1x10 THT female",
  J2: "P2.54mm 1x10 THT female",
  J_EXT_PWR: "P2.54mm 1x3 THT male",
  J_SUPERCAP: "P2.54mm 1x3 THT male",
  C_SUPERCAP: "Radial 2.00mm pitch, 7mm body",
}

const footprintFor = (component) => {
  const reference = component.name
  if (footprintByReference[reference]) return footprintByReference[reference]
  if (reference.startsWith("J_ISO_")) return "P2.54mm 1x2 THT + shunt"
  if (reference.startsWith("TP_")) return "1.2mm PCB test pad"
  if (reference.startsWith("LED")) return "0603"
  if (["R_SHIELD", "R_LED_PWR", "R_DBG_RED", "R_DBG_GREEN", "R_LED1_EN", "R_LED1", "R_LED2_EN", "R_LED2", "R_SUPERCAP"].includes(reference)) return "0603"
  if (["C_SHIELD", "C_LDO_IN", "C_LDO_OUT"].includes(reference)) return "0603"
  if (["C_DBG_BULK", "C_TARGET_BULK"].includes(reference)) return "0805"
  if (reference.startsWith("R") || reference.startsWith("C")) return "0402"
  return component.ftype ?? "unspecified"
}

const valueFor = (component) =>
  component.display_resistance ??
  component.display_capacitance ??
  component.symbol_display_value ??
  component.manufacturer_part_number ??
  component.name

const csvCell = (value) => {
  const text = value == null ? "" : String(value)
  return /[",\n]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text
}
const csv = (rows) => rows.map((row) => row.map(csvCell).join(",")).join("\n") + "\n"

const assemblyRows = []
for (const pcbComponent of pcbComponents) {
  const source = sourceComponents.get(pcbComponent.source_component_id)
  if (!source || source.ftype === "simple_test_point") continue

  const lcscCandidates = source.supplier_part_numbers?.jlcpcb ?? []
  const dnp = Boolean(pcbComponent.do_not_place)
  let notes = ""
  if (source.ftype === "simple_led") {
    notes = `Verify selected LCSC candidate emits ${source.color}; tscircuit's generic LED suggestions are not color-qualified.`
  } else if (source.name.startsWith("J_ISO_")) {
    notes = "Fit header pins and install removable shunt for normal LaunchPad operation."
  } else if (!source.manufacturer_part_number && lcscCandidates.length === 0) {
    notes = "Select an equivalent part matching the footprint and electrical value."
  }

  assemblyRows.push({
    reference: source.name,
    value: valueFor(source),
    footprint: footprintFor(source),
    mpn: source.manufacturer_part_number ?? "",
    lcsc: lcscCandidates.join(";"),
    dnp,
    notes,
    x: pcbComponent.center.x,
    y: pcbComponent.center.y,
    layer: pcbComponent.layer,
    rotation: pcbComponent.rotation ?? 0,
  })
}

const grouped = new Map()
for (const row of assemblyRows) {
  const key = JSON.stringify([
    row.value,
    row.footprint,
    row.mpn,
    row.lcsc,
    row.dnp,
    row.notes,
  ])
  const group = grouped.get(key) ?? { ...row, references: [] }
  group.references.push(row.reference)
  grouped.set(key, group)
}

const bomRows = [
  ["Quantity", "References", "Value", "Footprint", "Manufacturer Part Number", "LCSC Candidates", "DNP", "Notes"],
  ...[...grouped.values()]
    .sort((a, b) => a.references[0].localeCompare(b.references[0], undefined, { numeric: true }))
    .map((row) => [
      row.references.length,
      row.references.join(" "),
      row.value,
      row.footprint,
      row.mpn,
      row.lcsc,
      row.dnp ? "YES" : "NO",
      row.notes,
    ]),
]

const pnpHeader = ["Designator", "Mid X (mm)", "Mid Y (mm)", "Layer", "Rotation", "DNP"]
const pnpData = assemblyRows
  .sort((a, b) => a.reference.localeCompare(b.reference, undefined, { numeric: true }))
const pnpRowsLocal = [
  pnpHeader,
  ...pnpData.map((row) => [
    row.reference,
    row.x.toFixed(4),
    row.y.toFixed(4),
    row.layer === "bottom" ? "Bottom" : "Top",
    ((row.rotation % 360) + 360) % 360,
    row.dnp ? "YES" : "NO",
  ]),
]

// The optional KiCad export translates the board by (+100,+100) and KiCad's
// position exporter expresses Y with the opposite sign. Keep this as a
// separate reference file; the production Gerbers use tscircuit coordinates.
const pnpRowsKicad = [
  ["Designator", "Mid X (mm)", "Mid Y (mm)", "Layer", "Rotation", "DNP"],
  ...pnpData.map((row) => [
    row.reference,
    (row.x + 100).toFixed(4),
    (row.y - 100).toFixed(4),
    row.layer === "bottom" ? "Bottom" : "Top",
    ((row.rotation % 360) + 360) % 360,
    row.dnp ? "YES" : "NO",
  ]),
]

mkdirSync(outputDir, { recursive: true })
writeFileSync(resolve(outputDir, "bom.csv"), csv(bomRows))
writeFileSync(resolve(outputDir, "pick-and-place.csv"), csv(pnpRowsLocal))
writeFileSync(resolve(outputDir, "pick-and-place-tscircuit-local.csv"), csv(pnpRowsLocal))
writeFileSync(resolve(outputDir, "pick-and-place-kicad.csv"), csv(pnpRowsKicad))

console.log(`Wrote ${bomRows.length - 1} BOM groups and ${pnpRowsLocal.length - 1} placements to ${outputDir}`)