krishnax12/tensa-zangetsu-keychain-nfc

Summary: Calculates target inductance and estimated resonance for a 14.5 MHz square antenna using chip, interturn, bridge, and connection capacitances.

Version
0.0.13
License
unset
Stars
1

scripts/check-antenna-clearance.mjs

import assert from "node:assert/strict"

function pointSegmentDistance({ point, start, end }) {
  const dx = end.x - start.x
  const dy = end.y - start.y
  const lengthSquared = dx * dx + dy * dy
  const fraction = lengthSquared === 0 ? 0 : Math.max(0, Math.min(1,
    ((point.x - start.x) * dx + (point.y - start.y) * dy) / lengthSquared,
  ))
  return Math.hypot(point.x - start.x - fraction * dx, point.y - start.y - fraction * dy)
}

function cross({ start, end, point }) {
  return (end.x - start.x) * (point.y - start.y) - (end.y - start.y) * (point.x - start.x)
}

function segmentDistance({ start, end, coilStart, coilEnd }) {
  if (
    cross({ start, end, point: coilStart }) * cross({ start, end, point: coilEnd }) < 0 &&
    cross({ start: coilStart, end: coilEnd, point: start }) * cross({ start: coilStart, end: coilEnd, point: end }) < 0
  ) return 0
  return Math.min(
    pointSegmentDistance({ point: start, start: coilStart, end: coilEnd }),
    pointSegmentDistance({ point: end, start: coilStart, end: coilEnd }),
    pointSegmentDistance({ point: coilStart, start, end }),
    pointSegmentDistance({ point: coilEnd, start, end }),
  )
}

export function checkAntennaClearance(circuitJson) {
  const traces = circuitJson.filter((element) => element.type === "pcb_trace")
  const coil = traces.find((trace) => !trace.source_trace_id)
  assert(coil, "Missing printed antenna")
  let minimumGap = Infinity
  for (const trace of traces.filter((element) => element.source_trace_id)) {
    const first = trace.route[0]
    const last = trace.route.at(-1)
    const coilStart = coil.route[0]
    const coilEnd = coil.route.at(-1)
    const connectsOuter = [first, last].some((point) => Math.hypot(point.x - coilStart.x, point.y - coilStart.y) < 1e-5)
    const connectsInner = [first, last].some((point) => Math.hypot(point.x - coilEnd.x, point.y - coilEnd.y) < 1e-5)
    assert(connectsOuter || connectsInner, "RF trace does not terminate at the coil")
    for (let coilIndex = 0; coilIndex < coil.route.length - 1; coilIndex++) {
      // Only the terminal escape is allowed to join its corresponding RF trace.
      // The winding segments remain obstacles even though they share a net.
      if (connectsOuter && coilIndex === 0) continue
      if (connectsInner && coilIndex === coil.route.length - 2) continue
      const coilStart = coil.route[coilIndex]
      const coilEnd = coil.route[coilIndex + 1]
      for (let index = 0; index < trace.route.length; index++) {
        const start = trace.route[index]
        const end = trace.route[index + 1]
        let gap
        if (start.route_type === "via" && [start.from_layer, start.to_layer].includes(coilStart.layer)) {
          gap = pointSegmentDistance({ point: start, start: coilStart, end: coilEnd }) - (start.via_diameter + coilStart.width) / 2
        } else if (start.route_type === "wire" && end?.route_type === "wire" && start.layer === coilStart.layer && end.layer === coilStart.layer) {
          gap = segmentDistance({ start, end, coilStart, coilEnd }) - (start.width + coilStart.width) / 2
        } else continue
        minimumGap = Math.min(minimumGap, gap)
        assert(gap >= 0.15 - 1e-5, `${trace.source_trace_id} touches or approaches winding ${coilIndex}: gap ${gap.toFixed(6)} mm`)
      }
    }
  }
  assert(Number.isFinite(minimumGap))
  return { minimum_rf_trace_to_winding_gap_mm: minimumGap }
}