mohan-bee/relay-module

A tscircuit package created by mohan-bee.

Version
1.0.0
License
unset
Stars
0

verify.mjs

import assert from "node:assert/strict"
import { readFileSync, readdirSync, statSync } from "node:fs"
import { dirname, join } from "node:path"
import { fileURLToPath } from "node:url"

const circuit = JSON.parse(readFileSync(new URL("./dist/index/circuit.json", import.meta.url), "utf8"))
const near = (a, b) => Math.abs(a - b) < 1e-6
const boards = circuit.filter(e => e.type === "pcb_board")
assert.equal(boards.length, 1)
assert(near(boards[0].width, 50) && near(boards[0].height, 38) && near(boards[0].thickness, 1.6), "Board must be 50 × 38 × 1.6 mm")
const errors = circuit.filter(e => e.type.includes("error"))
assert.equal(errors.length, 0, JSON.stringify(errors, null, 2))

const holes = circuit.filter(e => e.type === "pcb_hole")
assert.equal(holes.length, 4)
for (const x of [-22, 22]) for (const y of [-16, 16]) {
  assert(holes.some(h => near(h.x, x) && near(h.y, y) && h.hole_shape === "circle" && near(h.hole_diameter, 3)), `Missing Ø3 mounting hole (${x}, ${y})`)
}
const components = circuit.filter(e => e.type === "source_component")
assert.equal(components.length, 18, "All 18 physical components must be present")
const componentDirectory = fileURLToPath(new URL("./components", import.meta.url))
const importFiles = directory => readdirSync(directory, { withFileTypes: true }).flatMap(entry => entry.isDirectory() ? importFiles(join(directory, entry.name)) : entry.name.endsWith(".tsx") ? [join(directory, entry.name)] : [])
const importedParts = new Set(importFiles(componentDirectory).map(file => {
  const source = readFileSync(file, "utf8")
  const code = source.match(/"jlcpcb"\s*:\s*\[\s*"(C\d+)"/)
  assert(code, `${file} must retain JLCPCB import provenance`)
  for (const match of source.matchAll(/from\s+"(\.\/.+\.(?:obj|step))"/g)) assert(statSync(join(dirname(file), match[1])).size > 0, `Missing imported CAD asset ${match[1]}`)
  return code[1]
}))
for (const c of components) {
  const codes = c.supplier_part_numbers?.jlcpcb
  assert(codes?.length && codes.every(code => importedParts.has(code)), `${c.name} must use a component imported from JLCPCB`)
  const cad = circuit.find(e => e.type === "cad_component" && e.source_component_id === c.source_component_id)
  assert(cad?.model_obj_url && cad.model_step_url, `${c.name} must retain its imported OBJ/STEP model`)
  for (const url of [cad.model_obj_url, cad.model_step_url]) assert(url.startsWith("https://"), `${c.name} model URL must be absolute so static --site builds can load it: ${url}`)
}
const mainSource = readFileSync(new URL("./index.circuit.tsx", import.meta.url), "utf8")
assert(!/<(?:chip|resistor|led|diode|transistor|pinheader)\b/.test(mainSource), "Use imported components for every physical part")
const pcbComponents = circuit.filter(e => e.type === "pcb_component")
const ports = circuit.filter(e => e.type === "source_port")
const nets = circuit.filter(e => e.type === "source_net")
const component = (name) => {
  const c = components.find(c => c.name === name)
  assert(c, `Missing ${name}`)
  return c
}
const port = (name, pin) => {
  const p = ports.find(p => p.source_component_id === component(name).source_component_id && p.pin_number === pin)
  assert(p, `Missing ${name} pin ${pin}`)
  return p
}
const net = (name) => {
  const n = nets.find(n => n.name === name)
  assert(n, `Missing ${name}`)
  return n
}
const expectNet = (name, pin, netName) => assert.equal(port(name, pin).subcircuit_connectivity_map_key, net(netName).subcircuit_connectivity_map_key, `${name}.${pin} must connect to ${netName}`)
for (const [name, assignments] of [["JIN", ["VCC", "IN2", "IN1", "GND"]], ["JPOWER", ["JDVCC", "VCC", "GND"]]]) {
  assignments.forEach((netName, i) => expectNet(name, i + 1, netName))
}

for (const ch of [1, 2]) {
  const id = component(`K${ch}`).source_component_id
  const pcb = pcbComponents.find(c => c.source_component_id === id)
  assert(pcb?.footprint_origin)
  const relayHoles = circuit.filter(e => e.type === "pcb_plated_hole" && e.pcb_component_id === pcb.pcb_component_id)
  assert.equal(relayHoles.length, 5)
  // Exact C35449 EasyEDA footprint, rotated 180° so its coil faces the drivers.
  assert(near(pcb.rotation, 180))
  for (const [pin, x, y] of [[1, 5.100066, 5.999988], [4, 5.100066, -5.999988], [5, 7.100062, 0], [3, -7.100062, -5.99948], [2, -7.100062, 5.99948]]) {
    const h = relayHoles.find(h => h.port_hints?.includes(`pin${pin}`))
    assert(h && near(h.x - pcb.footprint_origin.x, x) && near(h.y - pcb.footprint_origin.y, y) && near(h.hole_diameter, 1.524), `K${ch}.${pin} drill position/diameter`)
  }
  for (const [pin, name] of [[1, "JDVCC"], [4, `COIL${ch}`], [5, `COM${ch}`], [3, `NC${ch}`], [2, `NO${ch}`]]) expectNet(`K${ch}`, pin, name)
  expectNet(`U${ch}`, 2, `IN${ch}`)
  expectNet(`U${ch}`, 4, "JDVCC")
  expectNet(`Q${ch}`, 1, `BASE${ch}`)
  expectNet(`Q${ch}`, 2, "GND")
  expectNet(`Q${ch}`, 3, `COIL${ch}`)
  for (const [contact, relayPin, terminalPin] of [["NO", 2, 1], ["COM", 5, 2], ["NC", 3, 3]]) {
    const n = net(`${contact}${ch}`)
    expectNet(`J${ch}`, terminalPin, n.name)
    const members = ports.filter(p => p.subcircuit_connectivity_map_key === n.subcircuit_connectivity_map_key).map(p => p.source_port_id).sort()
    assert.deepEqual(members, [port(`K${ch}`, relayPin).source_port_id, port(`J${ch}`, terminalPin).source_port_id].sort(), `${n.name} must remain isolated from GPIO and driver nets`)
    const routes = circuit.filter(e => e.type === "pcb_trace" && e.connection_name === n.source_net_id)
    assert(routes.length > 0, `${n.name} must be routed`)
    for (const route of routes) for (const point of route.route) if (point.route_type === "wire") assert(near(point.width, 1.5), `${n.name} contact trace must be 1.5 mm`)
  }
}

console.log("PASS: all 18 parts have JLCPCB import provenance; board, mounting drills, imported relay pinout, driver/GPIO isolation, 1.5 mm contact routes, zero errors.")