pixalynx/pixal-nfc-card

This code defines React components representing specific electronic hardware components—namely, a 100-ohm SMD resistor, a surface-mount LED, and a chip with labeled pins—each modeled with detailed footprints, 3D CAD models, and manufacturer part information for PCB design.

Version
1.1.0
License
unset
Stars
0

scripts/audit-coil.ts

// Independent geometry audit of the antenna coil in a built circuit.json.
// tscircuit's DRC treats the whole coil as one net, so it cannot see a
// turn-to-turn short or a coil that misses its terminals; this does.
//
//   bun scripts/audit-coil.ts [dist/index/circuit.json]

import { readFileSync } from "node:fs"
import type { Pt } from "../lib/coil"
import { estimateInductance, resonanceHz } from "../lib/inductance"
import { CARD, COIL, TERMINAL } from "../lib/layout"

type Seg = [Pt, Pt]

const cross = (o: Pt, a: Pt, b: Pt) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x)
const ptSeg = (p: Pt, a: Pt, b: Pt) => {
  const dx = b.x - a.x
  const dy = b.y - a.y
  const len2 = dx * dx + dy * dy
  const t = len2 === 0 ? 0 : Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2))
  return Math.hypot(p.x - (a.x + t * dx), p.y - (a.y + t * dy))
}
const segDist = ([a, b]: Seg, [c, d]: Seg) => {
  const d1 = cross(c, d, a)
  const d2 = cross(c, d, b)
  const d3 = cross(a, b, c)
  const d4 = cross(a, b, d)
  if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) return 0
  return Math.min(ptSeg(a, c, d), ptSeg(b, c, d), ptSeg(c, a, b), ptSeg(d, a, b))
}

/** Smallest copper-to-copper gap between coil segments that do not share a corner. */
function minTurnGap(route: Pt[], width: number) {
  const segs: Seg[] = route.slice(1).map((b, i) => [route[i], b])
  let min = Infinity
  let where = ""
  for (let i = 0; i < segs.length; i++) {
    for (let j = i + 2; j < segs.length; j++) {
      const g = segDist(segs[i], segs[j]) - width
      if (g < min) {
        min = g
        where = `segments ${i} and ${j}`
      }
    }
  }
  return { min, where }
}

const file = process.argv[2] ?? "dist/index/circuit.json"
const circuit: any[] = JSON.parse(readFileSync(file, "utf8"))
const failures: string[] = []
const check = (ok: boolean, msg: string) => {
  console.log(`${ok ? "PASS" : "FAIL"}  ${msg}`)
  if (!ok) failures.push(msg)
}

const l1Source = circuit.find((e) => e.type === "source_component" && e.name === "L1")
const l1 = circuit.find((e) => e.type === "pcb_component" && e.source_component_id === l1Source?.source_component_id)
const coilTrace = circuit.find((e) => e.type === "pcb_trace" && e.pcb_component_id === l1?.pcb_component_id)
if (!coilTrace) {
  console.error(`No L1-owned coil trace found in ${file}`)
  process.exit(1)
}
const wires = coilTrace.route.filter((p: any) => p.route_type === "wire")
const route: Pt[] = wires.map((p: any) => ({ x: p.x, y: p.y }))
const holes = circuit.filter((e) => e.type === "pcb_plated_hole" && e.pcb_component_id === l1.pcb_component_id)

check(wires.every((p: any) => p.layer === "bottom"), `coil is entirely on the bottom layer (${route.length} points)`)
check(wires.every((p: any) => Math.abs(p.width - COIL.width) < 1e-6), `coil width is ${COIL.width} mm`)

for (const [label, end] of [["outer", route[0]], ["inner", route[route.length - 1]]] as const) {
  const hole = holes.find((h) => Math.hypot(h.x - end.x, h.y - end.y) < 1e-3)
  check(Boolean(hole), `${label} coil end (${end.x}, ${end.y}) lands on an L1 terminal hole`)
}

const turnGap = minTurnGap(route, COIL.width)
check(turnGap.min >= COIL.gap - 1e-6, `min turn-to-turn gap ${turnGap.min.toFixed(4)} mm (${turnGap.where}), target ${COIL.gap}`)

const segs: Seg[] = route.slice(1).map((b, i) => [route[i], b])
let holeGap = Infinity
for (const h of holes) {
  for (const s of segs) {
    const touches = [s[0], s[1]].some((p) => Math.hypot(p.x - h.x, p.y - h.y) < 1e-3)
    if (touches) continue
    holeGap = Math.min(holeGap, ptSeg(h, s[0], s[1]) - TERMINAL.outerDiameter / 2 - COIL.width / 2)
  }
}
check(holeGap >= 0.2, `min terminal-hole to other-turn gap ${holeGap.toFixed(4)} mm`)

const otherBottomCopper = circuit.filter(
  (e) =>
    (e.type === "pcb_smtpad" && e.layer === "bottom") ||
    (e.type === "pcb_trace" && e !== coilTrace && e.route.some((p: any) => p.layer === "bottom")) ||
    e.type === "pcb_copper_pour",
)
check(otherBottomCopper.length === 0, `no other copper on the bottom layer or pours near the coil (${otherBottomCopper.length})`)

const xs = route.map((p) => p.x)
const ys = route.map((p) => p.y)
const edge = Math.min(
  CARD.width / 2 - Math.max(...xs.map(Math.abs)),
  CARD.height / 2 - Math.max(...ys.map(Math.abs)),
) - COIL.width / 2
check(edge >= 1, `coil copper to board edge ${edge.toFixed(3)} mm (straight edges)`)

// The checker must catch a real short: push one right-hand leg onto its neighbour.
const shorted = route.map((p, i) => (i === 5 || i === 6 ? { x: p.x + COIL.width + COIL.gap, y: p.y } : p))
check(minTurnGap(shorted, COIL.width).min < 0, "self-test: an injected turn-to-turn short is detected")

const L = estimateInductance(route, COIL.width)
console.log(`\nEstimated L = ${(L * 1e6).toFixed(3)} uH (Greenhouse, no parasitics)`)
for (const pF of [47.5, 50, 52.5]) {
  console.log(`  CTUN ${pF} pF -> f0 ${(resonanceHz(L, pF * 1e-12) / 1e6).toFixed(2)} MHz`)
}

if (failures.length) {
  console.error(`\n${failures.length} coil check(s) failed`)
  process.exit(1)
}
console.log("\nCoil audit passed")