abse/dual-usb-peltier-assembly
A USB‑C PD Peltier controller PCB combining RP2040 control, adjustable buck‑boost TEC power, temperature sensing, fan regulation, USB programming, protection, status LEDs, and external accessory hardware.
- Version
- 0.4.6
- License
- unset
- Stars
- 0
tests/assembly.test.tsx
import { beforeAll, describe, expect, test } from "bun:test"
import { Circuit } from "tscircuit"
import { loadJscadPlan } from "circuit-json-to-gltf"
import Device from "../index.circuit"
import { boardFixture, renderFixture } from "./fixtures"
import { assemblyConnections, hotProbe, coldProbe, hotProbeCable, coldProbeCable, fan, fanCable } from "../src/assembly/models"
import provenance from "../assets/assembly/provenance.json"
type RenderedCircuit = InstanceType<typeof Circuit>
type MeshVertex = ReturnType<typeof loadJscadPlan>["triangles"][number]["vertices"][number]
let board: RenderedCircuit, device: RenderedCircuit
const originalNames = ["TEC_DEFAULT", "USB_PD_CABLE", "USB_PROGRAM_CABLE", "TEC_POS_LEAD", "TEC_NEG_LEAD"]
const externalNames = [...originalNames, "HOT_PROBE", "COLD_PROBE", "HOT_PROBE_CABLE", "COLD_PROBE_CABLE", "FAN_DEFAULT", "FAN_CABLE"]
const originalConnections = assemblyConnections.filter(connection => originalNames.includes(connection.partName))
const meshes = new Map<string, ReturnType<typeof loadJscadPlan>>()
beforeAll(async () => {
board = await boardFixture()
}, 60_000)
beforeAll(async () => {
device = await renderFixture(<Device schematicDisabled />)
}, 60_000)
function ownerName(c: RenderedCircuit, id: string | undefined) {
return c.db.source_component.list().find(part => part.source_component_id === id)?.name
}
function externalGeometry(name: string) {
const source = device.db.source_component.list().find(part => part.name === name)!
const cad = device.db.cad_component.list().find(model => model.source_component_id === source.source_component_id)!
let mesh = meshes.get(name)
if (!mesh) { mesh = loadJscadPlan(cad.model_jscad!); meshes.set(name, mesh) }
return { source, cad, mesh }
}
function physicalSignature(c: RenderedCircuit) {
return c.getCircuitJson().filter(item => item.type.startsWith("pcb_"))
.map(item => {
const value = item as unknown as Record<string, unknown>
const pcb = c.db.pcb_component.list().find(part => part.pcb_component_id === value.pcb_component_id)
return JSON.stringify({ name: pcb && ownerName(c, pcb.source_component_id),
...Object.fromEntries(Object.entries(value).filter(([key]) => !key.endsWith("_id") && !key.endsWith("_ids"))) })
}).sort()
}
function electricalSignature(c: RenderedCircuit) {
const roots = new Map<string, string>()
function root(id: string): string {
const parent = roots.get(id)
return !parent || parent === id ? id : root(parent)
}
const join = (ids: string[]) => {
if (!ids.length) return
const first = root(ids[0]!)
for (const id of ids.slice(1)) roots.set(root(id), first)
}
for (const trace of c.db.source_trace.list())
join([...trace.connected_source_port_ids, ...trace.connected_source_net_ids])
for (const part of c.db.source_component.list())
for (const ports of part.internally_connected_source_port_ids ?? []) join(ports)
return c.db.source_port.list().map(port => JSON.stringify({
name: ownerName(c, port.source_component_id), pin: port.pin_number,
nets: c.db.source_net.list().filter(net => root(net.source_net_id) === root(port.source_port_id)).map(net => net.name).sort(),
})).sort()
}
describe("native Peltier assembly", () => {
test("the default entry contains visible native models for the module, cables, temperature probes and fan", () => {
expect(device.firstChild?.componentName).toBe("AssemblyDevice")
expect(device.getCircuitJson().filter(item => item.type.endsWith("_error"))).toEqual([])
const external = device.db.source_component.list().filter(part => part.ftype === "subassembly")
expect(external.map(part => part.name).sort()).toEqual([...externalNames].sort())
for (const part of external) {
expect(part.supplier_part_numbers?.jlcpcb).toBeUndefined()
expect(device.db.pcb_component.list().filter(pcb => pcb.source_component_id === part.source_component_id)).toHaveLength(0)
expect(device.db.source_port.list().filter(port => port.source_component_id === part.source_component_id)).toHaveLength(0)
const cad = device.db.cad_component.list().find(model => model.source_component_id === part.source_component_id)!
expect(cad).toBeDefined()
expect(cad.pcb_component_id).toBeUndefined()
expect(cad.model_jscad).toBeDefined()
const { mesh } = externalGeometry(part.name)
expect(mesh.triangles.length).toBeGreaterThan(0)
expect(mesh.triangles.every(triangle => triangle.vertices.every(vertex =>
Number.isFinite(vertex.x) && Number.isFinite(vertex.y) && Number.isFinite(vertex.z)))).toBe(true)
for (const axis of ["x", "y", "z"] as const)
expect(mesh.boundingBox.max[axis] - mesh.boundingBox.min[axis]).toBeGreaterThan(0)
}
})
test("the mechanical assembly preserves the entire physical PCB and its electrical connections", () => {
expect(device.db.pcb_board.list()).toHaveLength(1)
expect(device.db.pcb_component.list().filter(part => !part.do_not_place)).toHaveLength(
board.db.pcb_component.list().filter(part => !part.do_not_place).length)
expect(device.db.pcb_component.list().filter(part => !part.do_not_place)).toHaveLength(134)
expect(device.db.pcb_component.list().filter(part => part.do_not_place)).toHaveLength(16)
expect(device.db.pcb_trace.list()).toHaveLength(0)
expect(device.db.pcb_via.list()).toHaveLength(0)
expect(physicalSignature(device)).toEqual(physicalSignature(board))
expect(electricalSignature(device)).toEqual(electricalSignature(board))
})
test("accessory solids meet the real connector endpoints with correct TEC polarity and documented dimensions", () => {
expect(originalConnections.map(connection => [connection.partName, connection.boardConnector,
"pin" in connection ? connection.pin : undefined])).toEqual([
["USB_PD_CABLE", "J_USB", undefined], ["USB_PROGRAM_CABLE", "J_PROGRAM", undefined],
["TEC_POS_LEAD", "J_TEC", "pin1"], ["TEC_NEG_LEAD", "J_TEC", "pin2"],
])
const { source: tecSource, cad: tecCad, mesh: tecMesh } = externalGeometry("TEC_DEFAULT")
expect(tecSource.display_name).toContain("TEC1-12706")
// loadJscadPlan converts circuit XY/Z into scene XZ/Y. Dimensions must
// match the selected product's recorded envelope, not just its CAD label.
const spans = (["x", "z", "y"] as const).map(axis => tecMesh.boundingBox.max[axis] - tecMesh.boundingBox.min[axis])
for (let i = 0; i < 3; i++) expect(spans[i]!).toBeCloseTo(provenance.peltier.bodyDimensionsMm[i]!, 5)
expect(provenance.peltier.source).toBe("https://www.amazon.com/dp/B00M1PM7H6")
expect(provenance.peltier.nativePart).toBe(tecSource.name)
expect(provenance.peltier.leadParts.map(lead => [lead.nativePart, lead.boardConnector, lead.pin])).toEqual(
originalConnections.flatMap(connection => "pin" in connection
? [[connection.partName, connection.boardConnector, connection.pin]] : []))
expect(provenance.usbCables.quantity).toBe(2)
expect(provenance.usbCables.roles.map(role => role.boardPort).sort()).toEqual(["J_PROGRAM", "J_USB"])
expect(provenance.usbCables.roles.map(role => [role.nativePart, role.boardPort])).toEqual(
originalConnections.filter(connection => !("pin" in connection)).map(connection => [connection.partName, connection.boardConnector]))
// Verify the rendered lead colors agree with their electrical polarity.
for (const [name, positive] of [["TEC_POS_LEAD", true], ["TEC_NEG_LEAD", false]] as const) {
const triangles = externalGeometry(name).mesh.triangles
expect(triangles.every(triangle => {
const color = triangle.material?.color
return Array.isArray(color) && (positive
? color[0] > color[1] + 0.3 && color[0] > color[2] + 0.3
: Math.max(...color) < 0.25)
})).toBe(true)
}
for (const connection of originalConnections) {
const part = device.db.source_component.list().find(part => part.name === connection.boardConnector)!
const pcb = device.db.pcb_component.list().find(pcb => pcb.source_component_id === part.source_component_id)!
if ("pin" in connection) {
const pin = Number(connection.pin.slice(3))
const sourcePort = device.db.source_port.list().find(port => port.source_component_id === part.source_component_id && port.pin_number === pin)!
const pcbPort = device.db.pcb_port.list().find(port => port.source_port_id === sourcePort.source_port_id)!
expect(connection.boardPoint[0]).toBeCloseTo(pcbPort.x, 5)
expect(connection.boardPoint[1]).toBeCloseTo(pcbPort.y, 5)
const trace = device.db.source_trace.list().find(trace => trace.connected_source_port_ids.includes(sourcePort.source_port_id))!
const net = device.db.source_net.list().find(net => trace.connected_source_net_ids.includes(net.source_net_id))!
expect(net.name).toBe(pin === 1 ? "TEC_POS" : "GND")
const point = connection.modulePoint
expect(point[0]).toBeCloseTo(tecCad.position.x + tecMesh.boundingBox.min.x, 5)
expect(point[1]).toBeGreaterThan(tecCad.position.y + tecMesh.boundingBox.min.z)
expect(point[1]).toBeLessThan(tecCad.position.y + tecMesh.boundingBox.max.z)
} else {
expect(pcb.cable_insertion_center).toBeDefined()
expect(connection.boardPoint[0]).toBeCloseTo(pcb.cable_insertion_center!.x, 5)
expect(connection.boardPoint[1]).toBeCloseTo(pcb.cable_insertion_center!.y, 5)
expect(externalGeometry(connection.partName).source.display_name).toContain("Cable Matters 201440")
}
const { cad, mesh } = externalGeometry(connection.partName)
expect(cad.rotation).toEqual({ x: 0, y: 0, z: 0 })
const offset = { x: cad.position.x, y: cad.position.z, z: cad.position.y }
// Check actual triangulated solids enclose both their board attachment
// and authored wire path; this catches an offset applied twice or lost.
for (const [x, circuitY, height] of [connection.boardPoint, ...connection.path]) {
const point = { x, y: height, z: circuitY }
for (const axis of ["x", "y", "z"] as const) {
expect(point[axis]).toBeGreaterThanOrEqual(mesh.boundingBox.min[axis] + offset[axis] - 0.01)
expect(point[axis]).toBeLessThanOrEqual(mesh.boundingBox.max[axis] + offset[axis] + 0.01)
}
}
}
})
test("both factory probe cables render three correctly colored conductors at the actual sensor terminals", () => {
const details = provenance.temperatureProbes
expect(details.source).toBe("https://www.amazon.com/dp/B00M1PM55K")
expect(details.quantityUsed).toBe(2)
expect(details.cableLengthMm).toBe(1_000)
expect(details.housingDimensionsVerified).toBe(false)
expect(details.roles.map(role => [role.role, role.boardPort, role.nativePart, role.cableNativePart, role.gpio])).toEqual([
["HOT", "J_HOT_SENSOR", "HOT_PROBE", "HOT_PROBE_CABLE", "GP8"],
["COLD", "J_COLD_SENSOR", "COLD_PROBE", "COLD_PROBE_CABLE", "GP9"],
])
expect(assemblyConnections.filter(connection => "probePoint" in connection).map(connection =>
[connection.partName, connection.boardConnector])).toEqual([
["HOT_PROBE_CABLE", "J_HOT_SENSOR"], ["COLD_PROBE_CABLE", "J_COLD_SENSOR"],
])
const pins = new Map(electricalSignature(device).map(json => {
const pin = JSON.parse(json) as { name: string; pin: number; nets: string[] }
return [`${pin.name}.${pin.pin}`, pin.nets] as const
}))
for (const [probe, cable, side] of [[hotProbe, hotProbeCable, "HOT"], [coldProbe, coldProbeCable, "COLD"]] as const) {
const { source: probeSource, cad: probeCad, mesh: probeMesh } = externalGeometry(probe.partName)
expect(probeSource.display_name).toContain("HiLetgo DS18B20")
expect(probeSource.display_name).toContain("illustrative")
expect(probeCad.position).toEqual({ x: probe.position[0], y: probe.position[1], z: probe.position[2] })
expect(probeMesh.boundingBox.max.x - probeMesh.boundingBox.min.x).toBeCloseTo(details.illustrativeHousingEnvelopeMm.length, 5)
for (const axis of ["y", "z"] as const)
expect(probeMesh.boundingBox.max[axis] - probeMesh.boundingBox.min[axis]).toBeCloseTo(details.illustrativeHousingEnvelopeMm.diameter, 5)
// The jacket joins the rear center of the actual rendered cylinder.
expect(cable.probePoint).toEqual(probe.cableEndPoint)
expect(cable.probePoint[0]).toBeCloseTo(probeCad.position.x + probeMesh.boundingBox.min.x, 5)
expect(cable.probePoint[1]).toBe(probeCad.position.y)
expect(cable.probePoint[2]).toBe(probeCad.position.z)
expect(cable.path[cable.path.length - 1]).toEqual(cable.probePoint)
expect(cable.conductors.map(conductor => [conductor.pin, conductor.signal, conductor.colorName])).toEqual([
["pin1", "CTRL_3V3", "red"], ["pin2", `${side}_SENSOR_DATA`, "yellow"], ["pin3", "GND", "black"],
])
const { cad, mesh } = externalGeometry(cable.partName)
expect(cad.rotation).toEqual({ x: 0, y: 0, z: 0 })
const offset = { x: cad.position.x, y: cad.position.z, z: cad.position.y }
const nearPoint = (vertices: MeshVertex[], point: readonly number[], radius: number) =>
vertices.some(vertex => Math.hypot(vertex.x + offset.x - point[0]!,
vertex.y + offset.y - point[2]!, vertex.z + offset.z - point[1]!) <= radius)
const connector = device.db.source_component.list().find(part => part.name === cable.boardConnector)!
for (const conductor of cable.conductors) {
const pin = Number(conductor.pin.slice(3))
const sourcePort = device.db.source_port.list().find(port =>
port.source_component_id === connector.source_component_id && port.pin_number === pin)!
const physicalPort = device.db.pcb_port.list().find(port => port.source_port_id === sourcePort.source_port_id)!
expect(conductor.boardPoint[0]).toBeCloseTo(physicalPort.x, 5)
expect(conductor.boardPoint[1]).toBeCloseTo(physicalPort.y, 5)
expect(pins.get(`${cable.boardConnector}.${pin}`)).toEqual([conductor.signal])
expect(conductor.path[0]).toEqual(conductor.boardPoint)
expect(conductor.path[conductor.path.length - 1]).toEqual(cable.path[0])
const [r, g, b] = conductor.color
expect(conductor.colorName === "red" ? r > g + 0.3 && r > b + 0.3 :
conductor.colorName === "yellow" ? r > 0.6 && g > 0.5 && b < 0.2 : Math.max(r, g, b) < 0.2).toBe(true)
const coloredVertices = mesh.triangles.filter(triangle => {
const color = triangle.material?.color
return Array.isArray(color) && conductor.color.every((value, i) => Math.abs(color[i]! - value) < 1e-6)
}).flatMap(triangle => triangle.vertices)
expect(coloredVertices.length).toBeGreaterThan(0)
// Matching-color polygons must physically reach every wire endpoint;
// a colored metadata label or a broad overall bounding box is insufficient.
for (const point of conductor.path) expect(nearPoint(coloredVertices, point, 0.36)).toBe(true)
}
const blackVertices = mesh.triangles.filter(triangle => {
const color = triangle.material?.color
return Array.isArray(color) && Math.max(...color.slice(0, 3)) < 0.2
}).flatMap(triangle => triangle.vertices)
for (const point of cable.path) expect(nearPoint(blackVertices, point, 1.51)).toBe(true)
}
})
test("manufacturer fan CAD and the four-wire factory plug match the real header without changing its NC signals", () => {
const details = provenance.fan
expect(details.source).toBe("https://www.amazon.com/dp/B00NEMG62M")
expect(details.dimensionsVerified).toBe(true)
expect(details.nativePart).toBe(fan.partName)
expect(details.cableNativePart).toBe(fanCable.partName)
expect(details.boardConnector).toBe(fanCable.boardConnector)
expect(details.nominalVoltageV).toBe(12)
expect(details.maximumInputCurrentA).toBeLessThan(details.boardContinuousFanBudgetA)
expect(details.cableLengthMm).toBe(fanCable.cableLengthMm)
expect(details.extensionFitted).toBe(false)
expect(details.lowNoiseAdaptorFitted).toBe(false)
const { source, cad, mesh } = externalGeometry(fan.partName)
expect(source.display_name).toContain("official NF-A8 family CAD")
expect(mesh.triangles.length).toBe(details.geometry.conversion.triangles)
expect(cad.position).toEqual({ x: fan.position[0], y: fan.position[1], z: fan.position[2] })
for (const [i, axis] of (["x", "z", "y"] as const).entries())
expect(mesh.boundingBox.max[axis] - mesh.boundingBox.min[axis]).toBeCloseTo(details.bodyWithPadsDimensionsMm[i]!, 4)
expect(fan.mountingHoleSpacingMm).toBe(details.mountingHoleSpacingMm)
// Exploded placement must leave the fan frame clear of the PCB and TEC.
const pcb = device.db.pcb_board.list()[0]!
expect(cad.position.y + mesh.boundingBox.max.z).toBeLessThan(pcb.center.y - pcb.height! / 2)
const tec = externalGeometry("TEC_DEFAULT")
expect(cad.position.y + mesh.boundingBox.max.z).toBeLessThan(tec.cad.position.y + tec.mesh.boundingBox.min.z)
expect(fanCable.path[fanCable.path.length - 1]).toEqual(fan.cableEndPoint)
// The unpublished cable exit is illustrative, but must meet the frame.
for (const [i, axis] of (["x", "z", "y"] as const).entries()) {
expect(fanCable.fanPoint[i]!).toBeGreaterThanOrEqual(fan.position[i]! + mesh.boundingBox.min[axis])
expect(fanCable.fanPoint[i]!).toBeLessThanOrEqual(fan.position[i]! + mesh.boundingBox.max[axis])
}
expect(fanCable.conductors.map(c => ({ pin: c.pin, signal: c.signal, color: c.colorName, boardConnected: c.boardConnected }))).toEqual(details.wireMap)
const connector = device.db.source_component.list().find(part => part.name === fanCable.boardConnector)!
const header = device.db.pcb_component.list().find(part => part.source_component_id === connector.source_component_id)!
const locator = device.db.pcb_hole.list().find(hole => hole.pcb_component_id === header.pcb_component_id)!
expect(locator.hole_shape === "circle" && locator.hole_diameter).toBe(1.25)
expect(fanCable.locatorPoint[0]).toBeCloseTo(locator.x, 5)
expect(fanCable.locatorPoint[1]).toBeCloseTo(locator.y, 5)
const cable = externalGeometry(fanCable.partName)
const nearPoint = (vertices: MeshVertex[], point: readonly number[], radius: number) => vertices.some(vertex =>
Math.hypot(vertex.x + cable.cad.position.x - point[0]!, vertex.y + cable.cad.position.z - point[2]!,
vertex.z + cable.cad.position.y - point[1]!) <= radius)
const pins = new Map(electricalSignature(device).map(json => {
const pin = JSON.parse(json) as { name: string; pin: number; nets: string[] }
return [`${pin.name}.${pin.pin}`, pin.nets] as const
}))
for (const conductor of fanCable.conductors) {
const pin = Number(conductor.pin.slice(3))
const sourcePort = device.db.source_port.list().find(port => port.source_component_id === connector.source_component_id && port.pin_number === pin)!
const physicalPort = device.db.pcb_port.list().find(port => port.source_port_id === sourcePort.source_port_id)!
expect(conductor.boardPoint[0]).toBeCloseTo(physicalPort.x, 5)
expect(conductor.boardPoint[1]).toBeCloseTo(physicalPort.y, 5)
expect(pins.get(`${fanCable.boardConnector}.${pin}`)).toEqual(conductor.boardConnected ? [conductor.signal] : [])
expect(Boolean(sourcePort.do_not_connect)).toBe(!conductor.boardConnected)
const vertices = cable.mesh.triangles.filter(triangle => {
const color = triangle.material?.color
return Array.isArray(color) && conductor.color.every((value, i) => Math.abs(color[i]! - value) < 1e-6)
}).flatMap(triangle => triangle.vertices)
expect(vertices.length).toBeGreaterThan(0)
for (const point of conductor.path) expect(nearPoint(vertices, point, 0.36)).toBe(true)
}
const sleeve = cable.mesh.triangles.filter(triangle => {
const color = triangle.material?.color
return Array.isArray(color) && [0.05, 0.055, 0.06].every((value, i) => Math.abs(color[i]! - value) < 1e-6)
}).flatMap(triangle => triangle.vertices)
for (const point of fanCable.path) expect(nearPoint(sleeve, point, 1.61)).toBe(true)
})
})