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
tests/board.test.ts
import { beforeAll, expect, test } from "bun:test"
import { Circuit } from "tscircuit"
import type { AnyCircuitElement } from "circuit-json"
import { convertCircuitJsonToPcbSvg } from "circuit-to-svg"
import Board from "../index.circuit"
import { schematicLayout } from "../schematic-layout"
import { pcbLayout } from "../pcb-layout"
import { mcuSignalFanouts } from "../mcu-signal-fanout"
import { renderSchematicSheetSvg } from "../lib/render-schematic-sheet-svg"
import { silkscreenLayout } from "../board-silkscreen"
import { getSilkscreenTextIssues } from "../lib/get-silkscreen-text-issues"
import { convertCircuitJsonToBomRows } from "circuit-json-to-bom-csv"
import { convertCircuitJsonToPickAndPlaceRows } from "circuit-json-to-pnp-csv"
import { resolveAssemblyPart } from "../lib/resolve-assembly-part"
import { assemblyParts } from "../assembly-parts"
let circuitJson: AnyCircuitElement[]
function expectPinNet(pin: { component: string; number: number }, netName: string) {
const component = circuitJson.find((element) => element.type === "source_component" && element.name === pin.component)
if (!component || component.type !== "source_component") throw new Error(`Missing ${pin.component}`)
const port = circuitJson.find((element) => element.type === "source_port" && element.source_component_id === component.source_component_id && element.pin_number === pin.number)
const net = circuitJson.find((element) => element.type === "source_net" && element.name === netName)
if (!port || port.type !== "source_port" || !net || net.type !== "source_net") throw new Error(`Missing pin or net for ${pin.component}.${pin.number}`)
expect(port.subcircuit_connectivity_map_key).toBeDefined()
expect(port.subcircuit_connectivity_map_key).toBe(net.subcircuit_connectivity_map_key)
}
beforeAll(async () => {
const circuit = new Circuit()
// These are source/placement regression tests. Full routing remains a separate gate.
circuit.pcbRoutingDisabled = true
circuit.add(Board())
await circuit.renderUntilSettled()
circuitJson = circuit.getCircuitJson()
}, 30_000)
test("the compiled board retains the requested fabrication minima", () => {
expect(circuitJson.find((element) => element.type === "pcb_board")).toMatchObject({
width: 60,
height: 38,
num_layers: 4,
solder_mask_color: "green",
min_via_hole_diameter: 0.3,
min_via_pad_diameter: 0.45,
allow_blind_and_buried_vias: false,
min_trace_width: 0.1,
min_trace_to_pad_edge_clearance: 0.09,
min_via_edge_to_pad_edge_clearance: 0.1,
min_pad_edge_to_pad_edge_clearance: 0.1,
min_via_hole_edge_to_via_hole_edge_clearance: 0.1,
min_plated_hole_drill_edge_to_drill_edge_clearance: 0.15,
min_board_edge_clearance: 0.2,
})
})
test("official assembly exporters exclude DNP/test pads and retain verified fitted metadata", async () => {
const originalCircuitJson = JSON.stringify(circuitJson)
const bomRows = await convertCircuitJsonToBomRows({ circuitJson, resolvePart: resolveAssemblyPart })
const pnpRows = convertCircuitJsonToPickAndPlaceRows(circuitJson, { supplier: "jlcpcb" })
expect(bomRows).toHaveLength(50)
expect(pnpRows).toHaveLength(50)
expect(bomRows.map(row => row.designator).sort()).toEqual(pnpRows.map(row => row.designator).sort())
for (const row of bomRows) {
expect(row.designator).not.toMatch(/^(TP\d|C23_|C24_)/)
const sourceComponent = circuitJson.find(element => element.type === "source_component" && element.name === row.designator)
if (sourceComponent?.type !== "source_component") throw new Error("Missing source component")
const part = assemblyParts.find(part => part.mpn === sourceComponent.manufacturer_part_number)
if (!part) throw new Error("Missing assembly part metadata")
expect(row.footprint).toBe(part.footprint)
expect(row.supplier_part_number_columns?.["JLCPCB Part #"]).toBe(part.jlcpcb)
}
for (const row of pnpRows) {
const sourceComponent = circuitJson.find(element => element.type === "source_component" && element.name === row.designator)
if (sourceComponent?.type !== "source_component") throw new Error("Missing source component")
const pcbComponent = circuitJson.find(element => element.type === "pcb_component" && element.source_component_id === sourceComponent.source_component_id)
if (pcbComponent?.type !== "pcb_component") throw new Error("Missing PCB component")
expect(row.layer).toBe("top")
expect(row.mid_x).toBe(pcbComponent.center.x)
expect(row.mid_y).toBe(pcbComponent.center.y)
// These imports lack supplier pin-1 calibration; preserve authored rotation.
// This assertion does not substitute for supplier orientation review.
expect(row.rotation).toBe(pcbComponent.rotation)
}
expect(JSON.stringify(circuitJson)).toBe(originalCircuitJson)
})
test("BOM resolver rejects unverified supplier substitutions", async () => {
const sourceComponent = circuitJson.find(element => element.type === "source_component" && element.name === "U1_MCU")
if (sourceComponent?.type !== "source_component") throw new Error("Missing U1")
await expect(resolveAssemblyPart({ source_component: { ...sourceComponent, supplier_part_numbers: { jlcpcb: ["C000000"] } } })).rejects.toThrow("Unverified MPN/supplier pairing")
})
test("silkscreen uses unique short references without changing component identities", () => {
const labels = circuitJson.filter(element => element.type === "pcb_silkscreen_text")
expect(labels).toHaveLength(65) // 58 references, title, antenna legend and five SWD functions.
for (const [name, placement] of Object.entries(silkscreenLayout)) {
const matching = labels.filter(label => label.text === name.split("_")[0])
expect(matching).toHaveLength(1)
expect(matching[0]).toMatchObject({ font_size: 0.9, ccw_rotation: 0, anchor_position: { x: placement.pcbX, y: placement.pcbY } })
expect(circuitJson.some(element => element.type === "source_component" && element.name === name)).toBe(true)
}
expect(labels.every(label => !label.text.includes("_"))).toBe(true)
})
test("silkscreen text stays clear of labels, pads, holes and component courtyards", () => {
expect(getSilkscreenTextIssues(circuitJson)).toEqual([])
})
test("the silkscreen check rejects an overlapping label", () => {
const label = circuitJson.find(element => element.type === "pcb_silkscreen_text" && element.text === "U2")
if (!label || label.type !== "pcb_silkscreen_text") throw new Error("Missing U2 label")
expect(getSilkscreenTextIssues([...circuitJson, { ...label, text: "CLASH" }])).toContain("Label overlap: CLASH / U2")
})
test("all component placements compile from the expanded functional layout", () => {
const sourceComponents = circuitJson.filter((element) => element.type === "source_component")
const allPcbComponents = circuitJson.filter((element) => element.type === "pcb_component")
const pcbComponents = allPcbComponents.filter((element) => sourceComponents.some((source) => source.source_component_id === element.source_component_id))
expect(Object.keys(pcbLayout)).toHaveLength(58)
expect(allPcbComponents).toHaveLength(74) // 58 parts/test pads plus sixteen native fanout via primitives.
expect(pcbComponents).toHaveLength(58)
for (const [name, placement] of Object.entries(pcbLayout)) {
const sourceComponent = sourceComponents.find((component) => component.name === name)
expect(sourceComponent).toBeDefined()
const pcbComponent = pcbComponents.find((component) => component.source_component_id === sourceComponent?.source_component_id)
expect(pcbComponent).toBeDefined()
// Imported asymmetric footprints have a bounding-box center distinct from
// their placement anchor; display offsets retain the authored coordinates.
expect(pcbComponent?.display_offset_x).toBeCloseTo(placement.pcbX, 5)
expect(pcbComponent?.display_offset_y).toBeCloseTo(placement.pcbY, 5)
}
expect(circuitJson.filter((element) => element.type.endsWith("_error"))).toEqual([])
})
test("the SWD fixture row is spaced at 2.2 mm on the MCU's SWD side", () => {
const pads = [pcbLayout.TP1_SWDIO, pcbLayout.TP2_SWDCK, pcbLayout.TP3_RESET, pcbLayout.TP4_GND, pcbLayout.TP5_SYS_VDD]
for (const [index, pad] of pads.entries()) {
expect(pad.pcbY).toBe(15.5)
expect(pad.pcbX).toBeCloseTo(14.4 + index * 2.2, 5)
expect(Math.hypot(pad.pcbX - pcbLayout.U1_MCU.pcbX, pad.pcbY - pcbLayout.U1_MCU.pcbY)).toBeLessThan(14)
}
})
test("the expanded PCB retains its reviewed component and keepout placement", () => {
const svg = convertCircuitJsonToPcbSvg(circuitJson, { width: 1200, height: 760, showCourtyards: true })
expect(svg).toMatchSnapshot("expanded-pcb-placement")
})
test("all 58 components are assigned to the three sheets and eight sections", () => {
const sheets = circuitJson.filter((element) => element.type === "schematic_sheet")
const components = circuitJson.filter((element) => element.type === "schematic_component")
expect(sheets).toHaveLength(3)
expect(components).toHaveLength(58)
for (const component of components) {
expect(sheets.some((sheet) => sheet.schematic_sheet_id === component.schematic_sheet_id)).toBe(true)
}
expect(Object.keys(schematicLayout)).toHaveLength(58)
expect(new Set(Object.values(schematicLayout).map((placement) => placement.schSectionName)).size).toBe(8)
for (const title of ["USB-C Input & Protection", "Charger & Power Path", "Battery & Fuel Gauge", "BLE MCU, Clock & Rails", "Motion Sensor", "SWD & Test Access", "Power-Gated Display", "2.4 GHz RF Front End"]) {
expect(circuitJson.some((element) => element.type === "schematic_text" && element.text === title)).toBe(true)
}
expect(circuitJson.filter((element) => element.type.includes("outside") && element.type.includes("schematic"))).toEqual([])
})
test("WSON charger, independent regulator and RF inductor match procurement codes", () => {
const components = circuitJson.filter((element) => element.type === "source_component")
expect(components.find((component) => component.name === "U2_CHARGER")).toMatchObject({
manufacturer_part_number: "BQ25186DLHR",
supplier_part_numbers: { jlcpcb: ["C44639442"] },
})
expect(components.find((component) => component.name === "U6_SYS_LDO")).toMatchObject({
manufacturer_part_number: "TPS7A0233DBVR",
supplier_part_numbers: { jlcpcb: ["C5142805"] },
})
expect(components.find((component) => component.name === "R6_CE_PU")).toMatchObject({
resistance: 100000,
})
expect(components.find((component) => component.name === "L2_RF_FILTER")).toMatchObject({
ftype: "simple_inductor",
inductance: "2.8nH",
manufacturer_part_number: "LQW15AN2N8G80D",
supplier_part_numbers: { jlcpcb: ["C412270"] },
})
})
test("both optional RF capacitors remain DNP", () => {
const components = circuitJson.filter((element) => element.type === "source_component")
for (const name of ["C23_RF_TUNE_IN", "C24_RF_TUNE_OUT"]) {
const sourceComponent = components.find((component) => component.name === name)
expect(sourceComponent).toBeDefined()
expect(circuitJson.find((element) => element.type === "pcb_component" && element.source_component_id === sourceComponent?.source_component_id)).toMatchObject({ do_not_place: true })
}
})
test("every charger and regulator pin follows the audited power tree", () => {
const chargerNets = ["VSYS_RAW", "BAT_SENSED", "CHG_PG_N", "CHG_CE_N", "GND", "BATT_NTC", "I2C_SDA", "I2C_SCL", "CHG_INT_N", "VBUS", "GND"]
for (const [index, netName] of chargerNets.entries()) {
expectPinNet({ component: "U2_CHARGER", number: index + 1 }, netName)
}
expectPinNet({ component: "U6_SYS_LDO", number: 1 }, "VSYS_RAW")
expectPinNet({ component: "U6_SYS_LDO", number: 2 }, "GND")
expectPinNet({ component: "U6_SYS_LDO", number: 3 }, "VSYS_RAW")
expectPinNet({ component: "U6_SYS_LDO", number: 5 }, "SYS_VDD")
const nets = circuitJson.filter((element) => element.type === "source_net")
expect(nets.find((net) => net.name === "VSYS_RAW")?.subcircuit_connectivity_map_key).not.toBe(nets.find((net) => net.name === "SYS_VDD")?.subcircuit_connectivity_map_key)
})
test("charge enable is pulled high and the NTC has no old parallel bias resistor", () => {
expectPinNet({ component: "R6_CE_PU", number: 1 }, "CHG_CE_N")
expectPinNet({ component: "R6_CE_PU", number: 2 }, "SYS_VDD")
expectPinNet({ component: "U1_MCU", number: 14 }, "CHG_CE_N")
expectPinNet({ component: "J2_BATTERY", number: 2 }, "BATT_NTC")
expectPinNet({ component: "SW1_WAKE", number: 1 }, "BATT_NTC")
const components = circuitJson.filter((element) => element.type === "source_component")
expect(components.some((component) => ["R4_TS_BIAS", "R3_IMAX", "C4_CHG_VDD"].includes(component.name))).toBe(false)
expect(components.some((component) => component.manufacturer_part_number === "BQ25150YFPR")).toBe(false)
})
test("both MCU ground pins retain authored copper connections to the ground net", () => {
expectPinNet({ component: "U1_MCU", number: 23 }, "GND")
expectPinNet({ component: "U1_MCU", number: 25 }, "GND")
expectPinNet({ component: "C13_VDDS1", number: 2 }, "GND")
for (const name of ["U1_GND_to_EP", "U1_EP_to_C13_GND"]) {
const source = circuitJson.find((element) => element.type === "source_trace" && element.name === name)
if (!source || source.type !== "source_trace") throw new Error(`Missing ${name}`)
const copper = circuitJson.find((element) => element.type === "pcb_trace" && element.source_trace_id === source.source_trace_id)
expect(copper).toBeDefined()
}
})
test("authored INT2 escape uses correctly placed full-stack 0.30/0.45 mm vias", () => {
expectPinNet({ component: "U1_MCU", number: 4 }, "ACCEL_INT2")
expectPinNet({ component: "U4_ACCEL", number: 6 }, "ACCEL_INT2")
const source = circuitJson.find((element) => element.type === "source_trace" && element.name === "U1_DIO11_to_U4_INT2")
if (!source || source.type !== "source_trace") throw new Error("Missing INT2 source trace")
const copper = circuitJson.find((element) => element.type === "pcb_trace" && element.source_trace_id === source.source_trace_id)
if (!copper || copper.type !== "pcb_trace") throw new Error("Missing INT2 copper")
const vias = circuitJson.filter((element) => element.type === "pcb_via").filter((via) => via.pcb_trace_id === copper.pcb_trace_id)
expect(vias).toHaveLength(2)
for (const via of vias) {
expect(via.hole_diameter).toBeGreaterThanOrEqual(0.3)
expect(via.outer_diameter).toBeGreaterThanOrEqual(0.45)
expect([...via.layers].sort()).toEqual(["bottom", "inner1", "inner2", "top"])
expect(new Set([via.from_layer, via.to_layer])).toEqual(new Set(["top", "bottom"]))
}
expect(vias[0]?.x).toBeCloseTo(20.8, 5)
expect(vias[0]?.y).toBeCloseTo(5.25, 5)
expect(vias[1]?.x).toBeCloseTo(24, 5)
expect(vias[1]?.y).toBeCloseTo(-5.5, 5)
expect(copper.route).toHaveLength(8)
for (const [index, point] of copper.route.entries()) {
if (point.route_type !== "via") continue
expect(copper.route[index - 1]).toMatchObject({ route_type: "wire", x: point.x, y: point.y, layer: point.from_layer })
expect(copper.route[index + 1]).toMatchObject({ route_type: "wire", x: point.x, y: point.y, layer: point.to_layer })
}
})
test("manual supply and signal vias all respect the through-via minima", () => {
expectPinNet({ component: "U1_MCU", number: 20 }, "VDDR")
expectPinNet({ component: "C11_VDDR_100N_B", number: 1 }, "VDDR")
expectPinNet({ component: "R13_I2C_SDA", number: 1 }, "I2C_SDA")
const bridgeSource = circuitJson.find((element) => element.type === "source_trace" && element.name === "VDDR_bottom_bridge")
if (!bridgeSource || bridgeSource.type !== "source_trace") throw new Error("Missing VDDR bridge source")
const bridgeCopper = circuitJson.find((element) => element.type === "pcb_trace" && element.source_trace_id === bridgeSource.source_trace_id)
if (!bridgeCopper || bridgeCopper.type !== "pcb_trace") throw new Error("Missing VDDR bridge copper")
expect(bridgeCopper.route.every((point) => point.route_type === "wire" && point.layer === "bottom" && point.width === 0.2)).toBe(true)
expect(bridgeCopper.route[0]).toMatchObject({ x: 17.35, y: 2.3 })
expect(bridgeCopper.route.at(-1)).toMatchObject({ x: 20.6, y: 4 })
const vias = circuitJson.filter((element) => element.type === "pcb_via")
expect(vias).toHaveLength(25)
for (const via of vias) {
expect(via.hole_diameter).toBeGreaterThanOrEqual(0.3)
expect(via.outer_diameter).toBeGreaterThanOrEqual(0.45)
expect(new Set(via.layers)).toEqual(new Set(["top", "inner1", "inner2", "bottom"]))
expect(new Set([via.from_layer, via.to_layer])).toEqual(new Set(["top", "bottom"]))
}
for (const copper of circuitJson.filter((element) => element.type === "pcb_trace")) {
for (const [index, point] of copper.route.entries()) {
if (point.route_type !== "via") continue
expect(copper.route[index - 1]).toMatchObject({ route_type: "wire", x: point.x, y: point.y, layer: point.from_layer })
expect(copper.route[index + 1]).toMatchObject({ route_type: "wire", x: point.x, y: point.y, layer: point.to_layer })
}
}
})
test("staggered MCU fanouts retain their intended nets and escape positions", () => {
for (const fanout of mcuSignalFanouts) {
const source = circuitJson.find((element) => element.type === "source_trace" && element.name === `${fanout.name}_escape`)
const net = circuitJson.find((element) => element.type === "source_net" && element.name === fanout.net)
if (!source || source.type !== "source_trace" || !net || net.type !== "source_net") throw new Error(`Missing ${fanout.name}`)
expect(source.subcircuit_connectivity_map_key).toBe(net.subcircuit_connectivity_map_key)
const copper = circuitJson.find((element) => element.type === "pcb_trace" && element.source_trace_id === source.source_trace_id)
if (!copper || copper.type !== "pcb_trace") throw new Error(`Missing escape copper ${fanout.name}`)
expect(copper.route.at(-1)).toMatchObject({
route_type: "wire", layer: "top",
x: pcbLayout.U1_MCU.pcbX + fanout.x, y: pcbLayout.U1_MCU.pcbY + fanout.y,
})
expect(copper.trace_length).toBeGreaterThan(0)
expect(copper.trace_length).toBeLessThan(2)
}
})
test("crystal connections remain short, continuous top-layer routes without vias", () => {
for (const name of ["U1_MCU_X48P_to_Y1_48MHZ_pin1", "U1_MCU_X48N_to_Y1_48MHZ_pin3"]) {
const source = circuitJson.find((element) => element.type === "source_trace" && element.name === name)
if (!source || source.type !== "source_trace") throw new Error(`Missing ${name}`)
const copper = circuitJson.find((element) => element.type === "pcb_trace" && element.source_trace_id === source.source_trace_id)
if (!copper || copper.type !== "pcb_trace") throw new Error(`Missing copper for ${name}`)
expect(copper.route.every((point) => point.route_type === "wire" && point.layer === "top")).toBe(true)
expect(copper.trace_length).toBeGreaterThan(0)
expect(copper.trace_length).toBeLessThan(6)
}
})
test("RF input reaches the filter through a short top-layer path", () => {
expectPinNet({ component: "U1_MCU", number: 1 }, "RF_FILTER_IN")
expectPinNet({ component: "L2_RF_FILTER", number: 1 }, "RF_FILTER_IN")
const source = circuitJson.find((element) => element.type === "source_trace" && element.name === "U1_ANT_to_L2_RF_FILTER")
if (!source || source.type !== "source_trace") throw new Error("Missing RF input connection")
const copper = circuitJson.find((element) => element.type === "pcb_trace" && element.source_trace_id === source.source_trace_id)
if (!copper || copper.type !== "pcb_trace") throw new Error("Missing RF input copper")
expect(copper.route).toHaveLength(4)
expect(copper.route.every((point) => point.route_type === "wire" && point.layer === "top" && point.width === 0.18)).toBe(true)
expect(copper.trace_length).toBeGreaterThan(0)
expect(copper.trace_length).toBeLessThan(4)
})
test("SWDIO reaches its fixture pad through two separated full-stack vias", () => {
expectPinNet({ component: "U1_MCU", number: 7 }, "SWDIO")
expectPinNet({ component: "TP1_SWDIO", number: 1 }, "SWDIO")
const source = circuitJson.find((element) => element.type === "source_trace" && element.name === "U1_SWDIO_to_TP1")
if (!source || source.type !== "source_trace") throw new Error("Missing SWDIO source")
const copper = circuitJson.find((element) => element.type === "pcb_trace" && element.source_trace_id === source.source_trace_id)
if (!copper || copper.type !== "pcb_trace") throw new Error("Missing SWDIO copper")
expect(copper.route).toHaveLength(9)
const vias = circuitJson.filter((element) => element.type === "pcb_via").filter((via) => via.pcb_trace_id === copper.pcb_trace_id)
expect(vias).toHaveLength(2)
for (const [index, point] of copper.route.entries()) {
if (point.route_type !== "via") continue
expect(copper.route[index - 1]).toMatchObject({ route_type: "wire", x: point.x, y: point.y, layer: point.from_layer })
expect(copper.route[index + 1]).toMatchObject({ route_type: "wire", x: point.x, y: point.y, layer: point.to_layer })
}
})
test("the VDDD route terminates on the decoupling capacitor's actual pad", () => {
const source = circuitJson.find((element) => element.type === "source_trace" && element.name === "U1_MCU_VDDD_to_C12_VDDD_pin1")
if (!source || source.type !== "source_trace") throw new Error("Missing VDDD source")
const copper = circuitJson.find((element) => element.type === "pcb_trace" && element.source_trace_id === source.source_trace_id)
if (!copper || copper.type !== "pcb_trace") throw new Error("Missing VDDD copper")
expect(copper.route).toHaveLength(3)
expect(copper.route.at(-1)).toMatchObject({ route_type: "wire", layer: "top", x: 13.920116, y: 5.8 })
expect(copper.trace_length).toBeGreaterThan(0)
expect(copper.trace_length).toBeLessThan(4)
})
test("SWD clock uses its dedicated escape and remains connected to TP2", () => {
expectPinNet({ component: "U1_MCU", number: 8 }, "SWDCK")
expectPinNet({ component: "TP2_SWDCK", number: 1 }, "SWDCK")
const source = circuitJson.find((element) => element.type === "source_trace" && element.name === "U1_SWDCK_to_TP2")
if (!source || source.type !== "source_trace") throw new Error("Missing SWD clock source")
const copper = circuitJson.find((element) => element.type === "pcb_trace" && element.source_trace_id === source.source_trace_id)
if (!copper || copper.type !== "pcb_trace") throw new Error("Missing SWD clock copper")
expect(copper.route).toHaveLength(12)
expect(copper.route.filter((point) => point.route_type === "via")).toHaveLength(2)
expect(copper.trace_length).toBeLessThan(25)
})
test("schematic sheets retain their reviewed visual layout", () => {
const sheets = circuitJson.filter((element) => element.type === "schematic_sheet")
for (const sheet of sheets) {
const svg = renderSchematicSheetSvg(circuitJson, sheet.schematic_sheet_id)
expect(svg).toMatchSnapshot(`schematic:${sheet.name}`)
}
})