0hmX/esp32-s3-usb-webcam-ov2640

This code defines and integrates a variety of surface-mount, through-hole, and connector hardware components (including microcontrollers, regulators, resistors, capacitors, and connectors) into a PCB design framework, enabling schematic symbol creation, footprint definition, and automated routing within an electronic circuit layout.

Version
1.0.7
License
unset
Stars
0

src/esp-en-via-postprocess-autorouter.ts

PCBPCB preview for src/esp-en-via-postprocess-autorouter.ts
SchematicSchematic preview for src/esp-en-via-postprocess-autorouter.ts
import {
  SOLVERS,
  type AutorouterCompleteEvent,
  type AutorouterErrorEvent,
  type AutorouterProgressEvent,
  type GenericLocalAutorouter,
  type SimpleRouteJson,
  type SimplifiedPcbTrace,
} from "tscircuit"

type EventHandlers = {
  complete: Array<(event: AutorouterCompleteEvent) => void>
  error: Array<(event: AutorouterErrorEvent) => void>
  progress: Array<(event: AutorouterProgressEvent) => void>
}

type RoutePoint = SimplifiedPcbTrace["route"][number]
type ViaOffset = { x: number; y: number }
const ESP_EN_SOURCE_NET_ID = "source_net_13"
const USB_DN_CONN_SOURCE_NET_ID = "source_net_5"
const USB_DP_CONN_SOURCE_NET_ID = "source_net_6"

function pointIsNear(
  point: RoutePoint | undefined,
  x: number,
  y: number,
  tolerance = 1e-3,
) {
  return (
    point !== undefined &&
    "x" in point &&
    "y" in point &&
    Math.abs(point.x - x) < tolerance &&
    Math.abs(point.y - y) < tolerance
  )
}

function routeJoins(
  trace: SimplifiedPcbTrace,
  a: { x: number; y: number },
  b: { x: number; y: number },
) {
  const first = trace.route[0]
  const last = trace.route.at(-1)
  return (
    (pointIsNear(first, a.x, a.y) && pointIsNear(last, b.x, b.y)) ||
    (pointIsNear(first, b.x, b.y) && pointIsNear(last, a.x, a.y))
  )
}

function wireAt(
  template: RoutePoint,
  x: number,
  y: number,
  layer: "top" | "bottom",
  width: number,
): RoutePoint {
  return {
    ...template,
    route_type: "wire",
    x,
    y,
    layer,
    width,
  } as RoutePoint
}

function viaAt(
  x: number,
  y: number,
  fromLayer: "top" | "bottom",
  toLayer: "top" | "bottom",
): RoutePoint {
  return {
    route_type: "via",
    x,
    y,
    from_layer: fromLayer,
    to_layer: toLayer,
    via_diameter: 0.45,
    via_hole_diameter: 0.2,
  } as RoutePoint
}

/**
 * Correct four deterministic clearances in the general routing phase after
 * the stable global solution has been found. The alternating USB D+/D- pad
 * pairs are joined on opposite sides, the connector GND2 escape is moved off
 * the VBUS bottom route, and the PWDN pull-down escape is moved away from the
 * bottom XCLK route.
 */
export function postprocessBoardClearances(
  traces: SimplifiedPcbTrace[],
): SimplifiedPcbTrace[] {
  let correctedDp = false
  let correctedDn = false
  let correctedUsbGround = false
  let correctedPwdnGround = false

  const result = traces.map((trace) => {
    if (
      trace.connection_name === USB_DP_CONN_SOURCE_NET_ID &&
      routeJoins(
        trace,
        { x: -26.1259568, y: 0.749936 },
        { x: -26.1259568, y: -0.250062 },
      )
    ) {
      correctedDp = true
      const first = trace.route[0]
      const last = trace.route.at(-1)!
      const startsAtDp1 = pointIsNear(first, -26.1259568, 0.749936)
      const start = startsAtDp1
        ? { x: -26.1259568, y: 0.749936 }
        : { x: -26.1259568, y: -0.250062 }
      const end = startsAtDp1
        ? { x: -26.1259568, y: -0.250062 }
        : { x: -26.1259568, y: 0.749936 }
      return {
        ...trace,
        route: [
          wireAt(first, start.x, start.y, "top", 0.15),
          wireAt(first, -27.6, start.y, "top", 0.15),
          wireAt(first, -27.6, end.y, "top", 0.15),
          wireAt(last, end.x, end.y, "top", 0.15),
        ],
      }
    }

    if (
      trace.connection_name === USB_DN_CONN_SOURCE_NET_ID &&
      routeJoins(
        trace,
        { x: -26.1259568, y: 0.250064 },
        { x: -26.1259568, y: 1.250062 },
      )
    ) {
      correctedDn = true
      const first = trace.route[0]
      const last = trace.route.at(-1)!
      const startsAtDn1 = pointIsNear(first, -26.1259568, 0.250064)
      const start = startsAtDn1
        ? { x: -26.1259568, y: 0.250064 }
        : { x: -26.1259568, y: 1.250062 }
      const end = startsAtDn1
        ? { x: -26.1259568, y: 1.250062 }
        : { x: -26.1259568, y: 0.250064 }
      return {
        ...trace,
        route: [
          wireAt(first, start.x, start.y, "top", 0.15),
          wireAt(first, -25.55, start.y, "top", 0.15),
          viaAt(-25.55, start.y, "top", "bottom"),
          wireAt(first, -25.55, start.y, "bottom", 0.15),
          wireAt(first, -25.55, end.y, "bottom", 0.15),
          viaAt(-25.55, end.y, "bottom", "top"),
          wireAt(first, -25.55, end.y, "top", 0.15),
          wireAt(last, end.x, end.y, "top", 0.15),
        ],
      }
    }

    const hasUsbGroundPad = trace.route.some((point) =>
      pointIsNear(point, -26.1258552, -2.7000063),
    )
    const hasUsbGroundEscape = trace.route.some(
      (point) =>
        point.route_type === "via" && pointIsNear(point, -25.1008692, -2.7000063),
    )
    if (hasUsbGroundPad && hasUsbGroundEscape) {
      correctedUsbGround = true
      const first = trace.route[0]
      const last = trace.route.at(-1)!
      const startsAtPad = pointIsNear(first, -26.1258552, -2.7000063)
      const movedVia: RoutePoint = {
        route_type: "via",
        x: -25.1008692,
        y: -3.5,
        from_layer: startsAtPad ? "top" : "inner1",
        to_layer: startsAtPad ? "inner1" : "top",
        via_diameter: 0.45,
        via_hole_diameter: 0.2,
      }
      const route = startsAtPad
        ? [
            wireAt(first, -26.1258552, -2.7000063, "top", 0.15),
            wireAt(first, -25.1008692, -2.7000063, "top", 0.15),
            wireAt(first, -25.1008692, -3.5, "top", 0.15),
            movedVia,
          ]
        : [
            movedVia,
            wireAt(first, -25.1008692, -3.5, "top", 0.15),
            wireAt(first, -25.1008692, -2.7000063, "top", 0.15),
            wireAt(last, -26.1258552, -2.7000063, "top", 0.15),
          ]
      return {
        ...trace,
        route,
      }
    }

    const hasPwdnGroundPad = trace.route.some((point) =>
      pointIsNear(point, -5.5, -9.175),
    )
    const hasPwdnGroundEscape = trace.route.some(
      (point) =>
        point.route_type === "via" && pointIsNear(point, -4.65, -9.175),
    )
    if (hasPwdnGroundPad && hasPwdnGroundEscape) {
      correctedPwdnGround = true
      return {
        ...trace,
        route: trace.route.map((point) =>
          pointIsNear(point, -4.65, -9.175)
            ? { ...point, x: -4.25, y: -9.175 }
            : point,
        ),
      }
    }

    return trace
  })

  if (
    !correctedDp ||
    !correctedDn ||
    !correctedUsbGround ||
    !correctedPwdnGround
  ) {
    throw new Error(
      `Board clearance postprocess did not find every target: DP=${correctedDp}, DN=${correctedDn}, USB_GND=${correctedUsbGround}, PWDN_GND=${correctedPwdnGround}`,
    )
  }
  return result
}

function postprocessBoardClearancesWhenPresent(
  input: SimpleRouteJson,
  traces: SimplifiedPcbTrace[],
) {
  const connectionNames = new Set(
    input.connections.map((connection) => connection.name),
  )
  return connectionNames.has(USB_DN_CONN_SOURCE_NET_ID) &&
    connectionNames.has(USB_DP_CONN_SOURCE_NET_ID)
    ? postprocessBoardClearances(traces)
    : traces
}

function isCoincident(a: RoutePoint, b: RoutePoint) {
  return (
    "x" in a &&
    "y" in a &&
    "x" in b &&
    "y" in b &&
    Math.abs(a.x - b.x) < 1e-9 &&
    Math.abs(a.y - b.y) < 1e-9
  )
}

function viaOverlapsRect(
  via: Extract<RoutePoint, { route_type: "via" }>,
  rect: SimpleRouteJson["obstacles"][number],
) {
  const radius = (via.via_diameter ?? 0) / 2
  const minX = rect.center.x - rect.width / 2
  const maxX = rect.center.x + rect.width / 2
  const minY = rect.center.y - rect.height / 2
  const maxY = rect.center.y + rect.height / 2
  const dx = Math.max(minX - via.x, 0, via.x - maxX)
  const dy = Math.max(minY - via.y, 0, via.y - maxY)
  return dx * dx + dy * dy < radius * radius
}

export function postprocessEspEnVia(
  input: SimpleRouteJson,
  traces: SimplifiedPcbTrace[],
  offsetMm: ViaOffset,
): SimplifiedPcbTrace[] {
  // Core's runtime SRJ deliberately carries Circuit JSON source identities,
  // not user-facing net labels or port selectors. The B7 circuit identifies
  // ESP_EN as source_net_13; keying on that net is stable across routed output
  // and avoids any dependency on generated pcb_via IDs.
  const espEnConnection = input.connections.find(
    (connection) => connection.name === ESP_EN_SOURCE_NET_ID,
  )
  if (!espEnConnection) {
    throw new Error("ESP_EN semantic net connection was not present")
  }
  const outputConnectionNames = new Set(
    [
      espEnConnection.name,
      espEnConnection.rootConnectionName,
      ...(espEnConnection.mergedConnectionNames ?? []),
    ].filter((name): name is string => Boolean(name)),
  )

  // Keepouts have no connectivity metadata in SRJ. The ESP32 antenna keepout
  // is the largest unconnected obstacle spanning every copper layer.
  const allLayerKeepout = input.obstacles
    .filter(
      (obstacle) =>
        obstacle.connectedTo.length === 0 &&
        ["top", "inner1", "inner2", "bottom"].every((layer) =>
          obstacle.layers.includes(layer),
        ),
    )
    .sort((a, b) => b.width * b.height - a.width * a.height)[0]
  if (!allLayerKeepout) {
    throw new Error("All-layer ESP32 antenna keepout was not present")
  }

  const collisions: Array<{
    traceIndex: number
    viaIndex: number
    via: Extract<RoutePoint, { route_type: "via" }>
  }> = []
  traces.forEach((trace, traceIndex) => {
    if (
      !trace.connection_name ||
      !outputConnectionNames.has(trace.connection_name)
    )
      return
    trace.route.forEach((point, viaIndex) => {
      if (
        point.route_type === "via" &&
        viaOverlapsRect(point, allLayerKeepout)
      ) {
        collisions.push({ traceIndex, viaIndex, via: point })
      }
    })
  })
  // The upstream router can now avoid the antenna keepout on its own when
  // nearby fixed traces change the topology. In that valid case the
  // postprocessor is deliberately a no-op. Retain the targeted correction
  // for the single-collision route shape this board historically produced.
  if (collisions.length === 0)
    return postprocessBoardClearancesWhenPresent(input, traces)
  if (collisions.length > 1) {
    throw new Error(
      `Expected at most one ESP_EN via/antenna collision, found ${collisions.length}`,
    )
  }

  const { traceIndex, viaIndex, via } = collisions[0]
  const trace = traces[traceIndex]
  const previous = trace.route[viaIndex - 1]
  const next = trace.route[viaIndex + 1]
  if (
    !previous ||
    previous.route_type !== "wire" ||
    !next ||
    next.route_type !== "wire" ||
    !isCoincident(previous, via) ||
    !isCoincident(next, via)
  ) {
    throw new Error("ESP_EN transition does not have coincident adjacent wires")
  }

  const movedX = via.x + offsetMm.x
  const movedY = via.y + offsetMm.y
  const movedVia = { ...via, x: movedX, y: movedY }
  if (viaOverlapsRect(movedVia, allLayerKeepout)) {
    throw new Error(
      `ESP_EN via still overlaps antenna keepout after (${offsetMm.x}, ${offsetMm.y})mm move`,
    )
  }

  const movedRoute = trace.route.map((point, pointIndex) => {
    if (
      pointIndex === viaIndex - 1 ||
      pointIndex === viaIndex ||
      pointIndex === viaIndex + 1
    ) {
      return { ...point, x: movedX, y: movedY }
    }
    return point
  })

  const espEnCorrected = traces.map((candidate, candidateIndex) =>
    candidateIndex === traceIndex
      ? { ...candidate, route: movedRoute }
      : candidate,
  )
  return postprocessBoardClearancesWhenPresent(input, espEnCorrected)
}

class EspEnViaPostprocessAutorouter implements GenericLocalAutorouter {
  readonly input: SimpleRouteJson
  isRouting = false
  private timer: ReturnType<typeof setTimeout> | undefined
  private cycleCount = 0
  private readonly solver: any
  private readonly handlers: EventHandlers = {
    complete: [],
    error: [],
    progress: [],
  }

  constructor(
    input: SimpleRouteJson,
    private readonly offsetMm: ViaOffset,
  ) {
    this.input = input
    this.solver = new SOLVERS.AutoroutingPipelineSolver7_MultiGraph(
      input as any,
      {},
    )
  }

  private emitError(caught: unknown) {
    const error = caught instanceof Error ? caught : new Error(String(caught))
    for (const handler of this.handlers.error) {
      handler({ type: "error", error })
    }
  }

  private async runCycle() {
    if (!this.isRouting) return
    try {
      if (this.solver.failed) {
        this.isRouting = false
        this.emitError(this.solver.error ?? "Routing failed")
        return
      }
      if (this.solver.solved) {
        this.isRouting = false
        const traces = postprocessEspEnVia(
          this.input,
          this.solver.getOutputSimpleRouteJson().traces ?? [],
          this.offsetMm,
        )
        for (const handler of this.handlers.complete) {
          handler({ type: "complete", traces })
        }
        return
      }

      const startedAt = Date.now()
      const startIterations = this.solver.iterations
      while (
        Date.now() - startedAt < 250 &&
        !this.solver.failed &&
        !this.solver.solved
      ) {
        if (typeof this.solver.stepAsync === "function") {
          await this.solver.stepAsync()
        } else {
          this.solver.step()
        }
      }
      this.cycleCount += 1
      const elapsed = Math.max(Date.now() - startedAt, 1)
      for (const handler of this.handlers.progress) {
        handler({
          type: "progress",
          steps: this.cycleCount,
          progress: this.solver.progress,
          phase: this.solver.getCurrentPhase(),
          iterationsPerSecond:
            ((this.solver.iterations - startIterations) / elapsed) * 1000,
          debugGraphics: this.solver.preview?.(),
        })
      }
      this.timer = setTimeout(() => void this.runCycle(), 0)
    } catch (error) {
      this.isRouting = false
      this.emitError(error)
    }
  }

  start() {
    if (this.isRouting) return
    this.isRouting = true
    void this.runCycle()
  }

  stop() {
    this.isRouting = false
    this.solver.stop?.()
    if (this.timer) clearTimeout(this.timer)
    this.timer = undefined
  }

  on(event: "complete", callback: (event: AutorouterCompleteEvent) => void): void
  on(event: "error", callback: (event: AutorouterErrorEvent) => void): void
  on(event: "progress", callback: (event: AutorouterProgressEvent) => void): void
  on(
    event: keyof EventHandlers,
    callback:
      | ((event: AutorouterCompleteEvent) => void)
      | ((event: AutorouterErrorEvent) => void)
      | ((event: AutorouterProgressEvent) => void),
  ) {
    ;(this.handlers[event] as Array<(event: any) => void>).push(callback)
  }

  solveSync() {
    this.solver.solve()
    if (this.solver.failed) {
      throw new Error(this.solver.error ?? "Routing failed")
    }
    return postprocessEspEnVia(
      this.input,
      this.solver.getOutputSimpleRouteJson().traces ?? [],
      this.offsetMm,
    )
  }
}

export function createEspEnViaPostprocessAutorouter(offsetMm: ViaOffset) {
  return async (input: SimpleRouteJson): Promise<GenericLocalAutorouter> =>
    new EspEnViaPostprocessAutorouter(input, offsetMm)
}