astra/am3352-sbc

A 4-layer AM3352 single-board computer with 128‑MiB DDR3, PMIC power regulation, USB‑C 5‑V PD input, dual USB host ports, HDMI output with ESD protection, microSD/UART/JTAG interfaces, addressable RGB indicators, and a PWM-driven buzzer.

Version
0.1.19
License
unset
Stars
0

scripts/build-board.tsx

// Optional local core build for validating an upstream fix without changing the lockfile.
const { Circuit }: typeof import("@tscircuit/core") = await import(process.env.SBC_CORE_PATH ?? "@tscircuit/core");
import { convertCircuitJsonToPcbSvg } from "circuit-to-svg";
import { Resvg } from "@resvg/resvg-js";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import type { FanoutTracePath } from "@tscircuit/props";
import Board from "../index.circuit";

const pairsOnly = process.argv.includes("--pairs-only");
const powerOnly = process.argv.includes("--power-only");
const placementOnly = process.argv.includes("--placement");
const ddrOnly = process.argv.includes("--ddr-only");
const stem = pairsOnly ? "board-pairs" : powerOnly
  ? "power-escapes"
  : placementOnly
    ? "placement"
    : ddrOnly
      ? "board-ddr"
      : "board";
await mkdir("output", { recursive: true });
const ddrRoutes: FanoutTracePath[] =
  placementOnly || powerOnly
    ? []
    : JSON.parse(await readFile("design/ddr-routes.json", "utf8"));
if (!placementOnly && !powerOnly && ddrRoutes.length !== 47)
  console.warn(
    "DDR remains unrouted on the requested four-layer stackup; no incompatible cached copper will be loaded.",
  );
const circuit = new Circuit();
circuit.schematicDisabled = true;
const asyncErrors: string[] = [];
circuit.on("asyncEffect:end", (event) => {
  if (event.error) asyncErrors.push(event.error);
});
const pendingWrites: Promise<void>[] = [];
let phase = 0;
if (process.argv.includes("--debug"))
  circuit.on("renderable:renderLifecycle:anyEvent", (event) => {
    if (event.componentDisplayName.startsWith("<board"))
      console.log(event.type);
  });
circuit.on("autorouting:start", (event) => {
  if (process.argv.includes("--export-lcd") && event.simpleRouteJson.connections.length === 20) {
    require("node:fs").writeFileSync("output/lcd-signal-first.srj.json", JSON.stringify(event.simpleRouteJson));
    require("node:fs").writeFileSync("output/lcd-signal-first.circuit.json", JSON.stringify(circuit.getCircuitJson()));
    process.exit(0);
  }
  if (process.argv.includes("--export-pairs") && event.simpleRouteJson.connections.length === 24) {
    require("node:fs").writeFileSync("output/peripheral-signal-first.srj.json", JSON.stringify(event.simpleRouteJson));
    require("node:fs").writeFileSync("output/peripheral-signal-first.audit.json", JSON.stringify(circuit.getCircuitJson()));
    process.exit(0);
  }
  console.log(
    `Phase ${++phase}: ${event.autorouterName}, ${event.simpleRouteJson.connections.length} connections`,
  );
  pendingWrites.push(
    writeFile(
      `output/${stem}-phase-${phase}.srj.json`,
      JSON.stringify(event.simpleRouteJson, null, 2),
    ),
  );
});
circuit.on("autorouting:end", (event) => {
  if (event.pcbTracePaths)
    pendingWrites.push(
      writeFile(
        `output/${stem}-phase-${phase}.paths.json`,
        JSON.stringify(event.pcbTracePaths, null, 2),
      ),
    );
});
circuit.add(
  <Board
    ddrRoutes={ddrRoutes}
    routePeripherals={!ddrOnly && !powerOnly && !pairsOnly}
    peripheralRoutes={!process.argv.includes("--fresh-peripherals") && !ddrOnly && !powerOnly && !placementOnly ? JSON.parse(await readFile("design/peripheral-routes.json", "utf8")) : []}
    placementOnly={placementOnly}
  />,
);
await circuit.renderUntilSettled();
await Promise.all(pendingWrites);
const json = circuit.getCircuitJson();
await writeFile(`output/${stem}.circuit.json`, JSON.stringify(json, null, 2));
const errors = json.filter((e) => e.type.endsWith("_error"));
const summary = {
  stage: stem,
  asyncErrors,
  components: json.filter((e) => e.type === "source_component").length,
  sourceTraces: json.filter((e) => e.type === "source_trace").length,
  pcbTraces: json.filter((e) => e.type === "pcb_trace").length,
  errorCounts: Object.fromEntries(
    [...new Set(errors.map((e) => e.type))].map((type) => [
      type,
      errors.filter((e) => e.type === type).length,
    ]),
  ),
};
console.log(JSON.stringify(summary, null, 2));
await writeFile(`output/${stem}-errors.json`, JSON.stringify(errors, null, 2));
await writeFile(
  `output/${stem}-summary.json`,
  JSON.stringify(summary, null, 2),
);
// Candidate checks use circuit JSON; render images only for requested previews.
if (!process.argv.includes("--no-images")) {
for (const layer of ["top", "inner1", "inner2", "bottom"] as const) {
  const svg = convertCircuitJsonToPcbSvg(json, {
    layer,
    width: 1500,
    height: 1200,
    hiddenLayerOpacity: 0.08,
  });
  await writeFile(`output/${stem}-${layer}.svg`, svg);
  await writeFile(
    `output/${stem}-${layer}.png`,
    new Resvg(svg).render().asPng(),
  );
}
}
const unexpectedCheckpointErrors = errors.filter(
  (e) =>
    !["pcb_port_not_connected_error", "pcb_trace_missing_error"].includes(
      e.type,
    ),
);
if (
  asyncErrors.length ||
  ((!ddrOnly || placementOnly) && errors.length) ||
  (ddrOnly && (ddrRoutes.length !== 47 || unexpectedCheckpointErrors.length))
)
  process.exitCode = 1;