AnasSarkiz/ble-pedometer

This code defines and configures various electronic hardware components such as surface-mount resistors, crystals, diodes, transistors, connectors, and switches with their physical footprints, schematic symbols, and 3D CAD models for PCB assembly.

Version
1.0.7
License
unset
Stars
0

lib/get-manufacturing-review.ts

import type { AnyCircuitElement } from "circuit-json"
import { convertCircuitJsonToGerberFiles } from "circuit-json-to-gerber"
import { convertCircuitJsonToBomRows, convertBomRowsToCsv } from "circuit-json-to-bom-csv"
import { convertCircuitJsonToPickAndPlaceRows, convertCircuitJsonToPickAndPlaceCsv } from "circuit-json-to-pnp-csv"
import { getFabricationBlockers } from "./get-fabrication-blockers"
import { resolveAssemblyPart } from "./resolve-assembly-part"

/** Official exporters consume the unchanged Circuit output. No CSV/geometry patches. */
export async function getManufacturingReview(circuitJson: AnyCircuitElement[]) {
  const blockers = getFabricationBlockers(circuitJson)
  if (blockers.length) throw new Error(blockers.join("\n"))
  const files = convertCircuitJsonToGerberFiles(circuitJson, { flip_y_axis: false })
  const platedDrillNames = Object.keys(files).filter(name => /^drill-L/.test(name))
  if (platedDrillNames.length !== 1 || platedDrillNames[0] !== "drill-L1-L4.drl" ||
      !files["drill-L1-L4.drl"].includes("TF.FileFunction,Plated,1,4,PTH")) {
    throw new Error(`Incorrect exported physical drill spans: ${platedDrillNames.join(", ")}`)
  }
  const bomRows = await convertCircuitJsonToBomRows({ circuitJson, resolvePart: resolveAssemblyPart })
  const pnpRows = convertCircuitJsonToPickAndPlaceRows(circuitJson, { supplier: "jlcpcb" })
  const fittedComponents = circuitJson.filter(element => element.type === "pcb_component")
    .filter(element => !element.do_not_place)
    .flatMap(pcbComponent => {
      const sourceComponent = circuitJson.find(element => element.type === "source_component" && element.source_component_id === pcbComponent.source_component_id)
      return sourceComponent?.type === "source_component" && sourceComponent.ftype !== "simple_test_point"
        ? [{ pcbComponent, sourceComponent }] : []
    })
  const fittedNames = fittedComponents.map(({ sourceComponent }) => sourceComponent.name).sort()
  if (fittedNames.length !== 50 || new Set(fittedNames).size !== 50 ||
      JSON.stringify(bomRows.map(row => row.designator).sort()) !== JSON.stringify(fittedNames) ||
      JSON.stringify(pnpRows.map(row => row.designator).sort()) !== JSON.stringify(fittedNames)) {
    throw new Error("BOM/PnP population mismatch: expected exactly the 50 fitted board components")
  }
  if (bomRows.some(row => !row.footprint || !row.supplier_part_number_columns?.["JLCPCB Part #"])) {
    throw new Error("Missing fitted-part package or supplier metadata")
  }
  if (pnpRows.some(row => row.layer !== "top" || !Number.isFinite(row.rotation) ||
      !Number.isFinite(row.mid_x) || !Number.isFinite(row.mid_y))) {
    throw new Error("Invalid placement coordinates, rotation or assembly side")
  }
  const orientationReviewRequired = fittedComponents.filter(({ pcbComponent }) =>
    !pcbComponent.pin1_location || !pcbComponent.supplier_pin1_location_map?.jlcpcb)
    .map(({ sourceComponent }) => sourceComponent.name)
  return {
    files: { ...files, "bom.csv": convertBomRowsToCsv(bomRows),
      "pick_and_place.csv": convertCircuitJsonToPickAndPlaceCsv(circuitJson, { supplier: "jlcpcb" }) },
    bomRows, pnpRows, orientationReviewRequired,
  }
}