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
PCB
Schematic
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)
}