astra/f1c100s
The code defines the physical pin layout, labels, and footprint for the F1C100S system-on-chip (SoC) component used in electronic devices.
- Version
- 0.9.2
- License
- unset
- Stars
- 0
tests/module.test.tsx
import { checkPreviewPours } from "../scripts/check-preview-pours";
import { checkCapacitorOrientation } from "../scripts/check-capacitor-orientation";
import { checkPlatedExits } from "../scripts/check-plated-exits";
import { checkConventionalRouting } from "../scripts/check-conventions";
import { test, expect } from "bun:test";
import {
Circuit,
type GenericLocalAutorouter,
type SimpleRouteJson,
type SimplifiedPcbTrace,
} from "tscircuit";
import { F1C100SModule, LAYOUT_PROFILES, getF1C100SCircuitJson } from "../src";
import { SCHEMATIC_BANK_PINS } from "../src/schematic";
import { PIN_NETS } from "../src/pin-map";
import { makeLayout } from "../src/layout";
import { MODULE_SIZE, TERMINAL_EDGE } from "../src/profiles";
import { validateCircuit } from "../scripts/validate";
for (const profile of LAYOUT_PROFILES)
test(`${profile}: stored copper loads and passes DRC`, async () => {
const c = new Circuit();
c.add(
<board
width={MODULE_SIZE}
height={MODULE_SIZE}
layers={4}
minViaPadDiameter={0.45}
minViaHoleDiameter={0.2}
autorouter={{
local: true,
algorithmFn: async () => {
throw new Error(
"Loading stored copper must not invoke an autorouter",
);
},
}}
>
<F1C100SModule name="SOC" layoutProfile={profile} />
</board>,
);
await c.renderUntilSettled();
const json = c.getCircuitJson();
// Check physical copper coverage independently of path orientation and
// of shared power branches repeated in several pin-to-exit paths.
const segments = (data: any[]) => {
const names = new Map(
data
.filter((e) => e.type === "source_trace")
.map((e) => [e.source_trace_id, e.name]),
);
return data
.filter((e) => e.type === "pcb_trace")
.flatMap((t) =>
t.route.flatMap((b: any, i: number) => {
const a = t.route[i - 1];
return a?.route_type === "wire" &&
b.route_type === "wire" &&
a.layer === b.layer &&
Math.hypot(a.x - b.x, a.y - b.y) > 1e-6
? [{ a, b, net: names.get(t.source_trace_id) }]
: [];
}),
);
};
const stored = getF1C100SCircuitJson(profile);
const loadedSegments = segments(json),
storedSegments = segments(stored);
for (const [from, to] of [
[loadedSegments, storedSegments],
[storedSegments, loadedSegments],
]) {
for (const s of from!)
for (const fraction of [0, 0.25, 0.5, 0.75, 1]) {
const x = s.a.x + (s.b.x - s.a.x) * fraction,
y = s.a.y + (s.b.y - s.a.y) * fraction;
expect(
to!.some((t) => {
if (t.net !== s.net || t.a.layer !== s.a.layer) return false;
const dx = t.b.x - t.a.x,
dy = t.b.y - t.a.y;
const f = Math.max(
0,
Math.min(
1,
((x - t.a.x) * dx + (y - t.a.y) * dy) / (dx * dx + dy * dy),
),
);
return Math.hypot(x - t.a.x - f * dx, y - t.a.y - f * dy) < 1e-5;
}),
).toBe(true);
}
}
const viaKeys = (data: any[]) =>
data
.filter((e) => e.type === "pcb_via")
.map(
(v) =>
`${v.x.toFixed(6)},${v.y.toFixed(6)},${v.hole_diameter},${v.outer_diameter}`,
)
.sort();
expect(viaKeys(json)).toEqual(viaKeys(stored));
expect(
json
.filter((e) => e.type === "pcb_trace")
.every((e: any) => e.pcb_trace_id.startsWith("saved_fanout_")),
).toBe(true);
const capacitors = json.filter(
(e: any) =>
e.type === "source_component" && e.ftype === "simple_capacitor",
);
expect(capacitors).toHaveLength(32);
for (const capacitor of capacitors as any[]) {
const placed = json.find(
(e: any) =>
e.type === "pcb_component" &&
e.source_component_id === capacitor.source_component_id,
) as any;
expect(placed.layer).toBe("top");
const pads = json.filter(
(e: any) =>
e.type === "pcb_smtpad" &&
e.pcb_component_id === placed.pcb_component_id,
) as any[];
expect(pads).toHaveLength(2);
expect(pads.every((p) => p.layer === "top")).toBe(true);
}
expect(checkConventionalRouting(json)).toEqual([]);
expect(checkCapacitorOrientation(json)).toEqual([]);
expect(checkPlatedExits(json)).toEqual([]);
expect(
json.filter(
(e: any) =>
e.type === "source_component" && e.ftype === "simple_resistor",
),
).toHaveLength(13);
const crystals = json.filter(
(e: any) => e.type === "source_component" && e.ftype === "simple_crystal",
) as any[];
expect(crystals).toHaveLength(1);
expect(crystals[0].frequency).toBe(24e6);
const sourcePorts = json.filter(
(e: any) => e.type === "source_port",
) as any[];
const sources = json.filter(
(e: any) => e.type === "source_component",
) as any[];
const netOf = (name: string, pin: number) => {
const source = sources.find((s) => s.name === name);
const port = sourcePorts.find(
(p) =>
p.source_component_id === source?.source_component_id &&
p.pin_number === pin,
);
return (
json.find(
(e: any) =>
e.type === "source_trace" &&
e.connected_source_port_ids.includes(port?.source_port_id),
) as any
)?.name;
};
expect(netOf("Y1", 1)).toBe("N_HOSCI");
expect(netOf("Y1", 3)).toBe("N_HOSCO");
for (const pin of [2, 4]) expect(netOf("Y1", pin)).toBe("N_GND");
for (const [name, net] of [
["C_OSCI", "HOSCI"],
["C_OSCO", "HOSCO"],
]) {
expect(
sources.find((s) => s.name === name).manufacturer_part_number,
).toBe("GRM1555C1H180JA01D");
expect(sources.find((s) => s.name === name).max_voltage_rating).toBe(50);
expect(netOf(name!, 1)).toBe(`N_${net}`);
expect(netOf(name!, 2)).toBe("N_GND");
expect(sources.find((s) => s.name === name).capacitance).toBeCloseTo(
18e-12,
15,
);
}
expect(netOf("C_TV_REF", 1)).toBe("N_TV_VRP");
expect(netOf("C_TV_REF", 2)).toBe("N_TV_VRN");
for (const net of [
"SDMMC0_CMD",
"SDMMC0_D0",
"SDMMC0_D1",
"SDMMC0_D2",
"SDMMC0_D3",
"SPI0_CS",
"TWI0_SDA",
"TWI0_SCL",
]) {
expect(netOf(`R_PU_${net}`, 1)).toBe("N_VCC_IO");
expect(netOf(`R_PU_${net}`, 2)).toBe(`N_${net}`);
expect(sources.find((s) => s.name === `R_PU_${net}`).resistance).toBe(
net.startsWith("SDMMC") ? 47000 : net === "SPI0_CS" ? 10000 : 4700,
);
}
const chip = json.find(
(e: any) => e.type === "source_component" && e.name === "U1",
) as any;
const physicalPorts = json.filter(
(e: any) =>
e.type === "source_port" &&
e.source_component_id === chip.source_component_id,
) as any[];
const renderedPorts = json.filter(
(e: any) => e.type === "schematic_port",
) as any[];
expect(physicalPorts).toHaveLength(89);
for (const port of physicalPorts) {
expect(
renderedPorts.filter((p) => p.source_port_id === port.source_port_id),
).toHaveLength(1);
}
expect(json.filter((e) => e.type === "schematic_component")).toHaveLength(
53,
);
expect(json.filter((e) => e.type.endsWith("_error"))).toEqual([]);
const errors = await validateCircuit(json);
if (errors.length) console.log(errors.slice(0, 5));
expect(errors).toEqual([]);
expect(json.filter((e) => e.type === "pcb_trace").length).toBeGreaterThan(
100,
);
expect(json.filter((e: any) => e.type === "pcb_autorouting_error")).toEqual(
[],
);
expect(
json.filter(
(e: any) => e.type === "source_component" && /^C_D\d+$/.test(e.name),
),
).toHaveLength(15);
if (profile !== "native") {
const [, lcdSide, , storageSide] = profile.split("_");
for (const [prefix, side] of [
["LCD_", lcdSide],
["SPI0_", storageSide],
["SDMMC0_", storageSide],
]) {
const sources = json.filter(
(e: any) =>
e.type === "source_component" && e.name.startsWith(prefix),
) as any[];
expect(sources.length).toBe(
prefix === "LCD_" ? 22 : prefix === "SPI0_" ? 4 : 6,
);
for (const source of sources) {
const placed = json.find(
(e: any) =>
e.type === "pcb_component" &&
e.source_component_id === source.source_component_id,
) as any;
const coord =
side === "left" || side === "right"
? placed.center.x
: placed.center.y;
expect(coord).toBeCloseTo(
(side === "left" || side === "bottom" ? -1 : 1) * TERMINAL_EDGE,
5,
);
}
}
}
}, 120000);
for (const parentLayer of ["top", "inner1", "inner2", "bottom"] as const)
test(`parent routing connects to a plated exit on ${parentLayer}`, async () => {
const terminal = makeLayout("lcd_top_storage_right").terminals.find(
(t) => t.name === "SPI0_CLK",
)!;
let captured: SimpleRouteJson | undefined;
const factory = async (
input: SimpleRouteJson,
): Promise<GenericLocalAutorouter> => {
captured = input;
const handlers: Record<string, ((e: any) => void)[]> = {
complete: [],
progress: [],
error: [],
};
const traces: SimplifiedPcbTrace[] = input.connections.map((c, i) => ({
type: "pcb_trace",
pcb_trace_id: `parent_${i}`,
connection_name: c.name,
route: c.pointsToConnect.map((p) => ({
route_type: "wire",
x: p.x,
y: p.y,
layer: parentLayer,
width: 0.12,
})),
}));
return {
input,
isRouting: false,
on(e: string, f: (e: any) => void) {
handlers[e]!.push(f);
},
start() {
queueMicrotask(() =>
handlers.complete!.forEach((f) => f({ type: "complete", traces })),
);
},
stop() {},
solveSync() {
return traces;
},
};
};
const c = new Circuit();
c.add(
<board
width={52}
height={36}
layers={4}
autorouter={{ local: true, algorithmFn: factory }}
>
<F1C100SModule
name="SOC"
layoutProfile="lcd_top_storage_right"
connections={{ SPI0_CLK: ".OUT > .pin1" }}
/>
<chip
name="OUT"
pinLabels={{ pin1: "pin1" }}
pcbX={22}
pcbY={terminal.y}
footprint={
<footprint>
<platedhole
portHints={["pin1"]}
shape="circle"
holeDiameter={0.2}
outerDiameter={0.45}
pcbX={0}
pcbY={0}
/>
</footprint>
}
/>
</board>,
);
await c.renderUntilSettled();
expect(captured?.connections).toHaveLength(1);
for (const p of captured!.connections[0]!.pointsToConnect) {
expect(p.layers).toContain(parentLayer);
const hole = captured!.obstacles.find(
(o) =>
o.circuitJsonMetadata?.pcb_plated_hole_id &&
Math.hypot(o.center.x - p.x, o.center.y - p.y) < 1e-6,
);
expect(hole?.layers).toContain(parentLayer);
}
const exit = captured!.connections[0]!.pointsToConnect.find(
(p) => Math.abs(p.x - TERMINAL_EDGE) < 1e-6,
)!;
expect(
new Set(
captured!.obstacles.find(
(o) =>
o.circuitJsonMetadata?.pcb_plated_hole_id &&
Math.hypot(o.center.x - exit.x, o.center.y - exit.y) < 1e-6,
)?.layers,
),
).toEqual(new Set(["top", "inner1", "inner2", "bottom"]));
const xs = captured!.connections[0]!.pointsToConnect.map((p) => p.x).sort(
(a, b) => a - b,
);
expect(xs).toEqual([TERMINAL_EDGE, 22]);
const errors = await validateCircuit(c.getCircuitJson());
if (errors.length) console.log(errors.slice(0, 5));
expect(errors).toEqual([]);
}, 120000);
test("the pin map covers every physical pin once", () => {
expect([...SCHEMATIC_BANK_PINS].sort((a, b) => a - b)).toEqual(
Array.from({ length: 89 }, (_, i) => i + 1),
);
expect(
Object.keys(PIN_NETS)
.map(Number)
.sort((a, b) => a - b),
).toEqual(Array.from({ length: 89 }, (_, i) => i + 1));
expect(
new Set(
Array.from(
{ length: 22 },
(_, i) =>
Object.values(PIN_NETS)
.filter((n) => n.startsWith("LCD_"))
.sort()[i],
),
).size,
).toBe(22);
});
test("data is cloned; unsupported profiles and legacy props fail explicitly", () => {
expect(() =>
F1C100SModule({ name: "SOC", busProfile: "native" } as any),
).toThrow("Use layoutProfile");
const a = getF1C100SCircuitJson("native"),
b = getF1C100SCircuitJson("native");
a.splice(0);
expect(b.length).toBeGreaterThan(100);
expect(() => getF1C100SCircuitJson("typo" as any)).toThrow("Unsupported");
expect(() =>
F1C100SModule({ name: "SOC", variant: "native" } as any),
).toThrow("Use layoutProfile");
expect(() =>
F1C100SModule({ name: "SOC", connections: { TYPO: "net.GND" } }),
).toThrow("Unknown");
});
test("two rotated instances retain independent nets and copper", async () => {
const c = new Circuit();
c.add(
<board width={82} height={42} layers={4}>
<F1C100SModule
name="A"
layoutProfile="lcd_top_storage_right"
pcbX={-20}
/>
<F1C100SModule
name="B"
layoutProfile="lcd_right_storage_left"
pcbX={20}
pcbRotation={90}
/>
</board>,
);
await c.renderUntilSettled();
const json = c.getCircuitJson();
expect(checkCapacitorOrientation(json)).toEqual([]);
const errors = await validateCircuit(json);
if (errors.length) console.log(errors.slice(0, 5));
expect(errors).toEqual([]);
expect(
json.filter(
(e: any) =>
e.type === "source_component" &&
e.manufacturer_part_number === "F1C100S",
),
).toHaveLength(2);
const ids = json
.filter((e: any) => e.type === "source_trace")
.map((e: any) => e.source_trace_id);
expect(new Set(ids).size).toBe(ids.length);
const copperIds = json
.filter((e: any) => e.type === "pcb_trace")
.map((e: any) => e.pcb_trace_id);
expect(new Set(copperIds).size).toBe(copperIds.length);
}, 120000);
test("exported schematic boxes share one processor across two A4 sheets", async () => {
const { ProfilePreview } = await import("../src/ProfilePreview");
const c = new Circuit();
c.add(<ProfilePreview layoutProfile="lcd_top_storage_right" />);
await c.renderUntilSettled();
const json = c.getCircuitJson() as any[];
expect(checkPreviewPours(json)).toEqual([]);
expect(json.filter((e) => e.type.endsWith("_error"))).toEqual([]);
expect(
json.filter((e) => e.type === "schematic_element_outside_sheet_warning"),
).toEqual([]);
const sheets = json.filter((e) => e.type === "schematic_sheet");
expect(sheets).toHaveLength(2);
expect(sheets.every((e) => e.sheet_size === "a4")).toBe(true);
const chips = json.filter(
(e) =>
e.type === "source_component" && e.manufacturer_part_number === "F1C100S",
);
expect(chips).toHaveLength(1);
const chip = chips[0];
const symbols = json.filter(
(e) =>
e.type === "schematic_component" &&
e.source_component_id === chip.source_component_id,
);
expect(symbols).toHaveLength(7);
expect(new Set(symbols.map((e) => e.schematic_sheet_id)).size).toBe(2);
expect(symbols.every((e) => e.size.width < 3 && e.size.height < 9)).toBe(
true,
);
const ports = json.filter(
(e) =>
e.type === "source_port" &&
e.source_component_id === chip.source_component_id,
);
expect(ports).toHaveLength(89);
for (const port of ports) {
expect(
json.filter(
(e) =>
e.type === "schematic_port" &&
e.source_port_id === port.source_port_id,
),
).toHaveLength(1);
}
const sheetIds = new Set(sheets.map((e) => e.schematic_sheet_id));
expect(json.filter((e) => e.type === "schematic_component")).toHaveLength(53);
expect(
json
.filter((e) =>
["schematic_component", "schematic_trace", "schematic_port"].includes(
e.type,
),
)
.every((e) => sheetIds.has(e.schematic_sheet_id)),
).toBe(true);
const errors = await validateCircuit(json);
expect(errors).toEqual([]);
}, 120000);
test("angle audit rejects oblique runs including via approaches", () => {
const route = (x: number, y: number, route_type = "wire") =>
[
{
type: "pcb_trace",
pcb_trace_id: "angle_fixture",
route: [
{ route_type: "wire", x: 0, y: 0, layer: "top" },
{
route_type,
x,
y,
layer: "top",
from_layer: "top",
to_layer: "bottom",
},
],
},
] as any;
for (const [x, y] of [
[1, 0],
[0, 1],
[1, 1],
[-1, 1],
[0, 0],
])
expect(checkConventionalRouting(route(x!, y!))).toEqual([]);
expect(checkConventionalRouting(route(1, 0.5))).toHaveLength(1);
expect(checkConventionalRouting(route(1, 0.5, "via"))).toHaveLength(1);
});
test("saved external-net paths end at their named plated exit", async () => {
for (const profile of LAYOUT_PROFILES) {
const json = getF1C100SCircuitJson(profile) as any[];
const paths = await Bun.file(
`src/generated/${profile}.trace-paths.json`,
).json();
for (const path of paths) {
const [name, pin] = path.connection.split(".pin");
const component = json.find(
(e) => e.type === "source_component" && e.name === name,
);
const port = json.find(
(e) =>
e.type === "source_port" &&
e.source_component_id === component.source_component_id &&
e.pin_number === Number(pin),
);
const net = json.find(
(e) =>
e.type === "source_trace" &&
e.connected_source_port_ids.includes(port.source_port_id),
);
const terminal = makeLayout(profile).terminals.find(
(t) => `N_${t.name}` === net.name,
);
if (!terminal) continue;
expect(path.route.at(-1).x).toBeCloseTo(terminal.x, 6);
expect(path.route.at(-1).y).toBeCloseTo(terminal.y, 6);
}
}
});
test("orientation audit catches reversed and sideways decoupling pads", () => {
const data: any[] = [
{
type: "source_component",
source_component_id: "chip",
source_group_id: "module",
name: "U1",
ftype: "simple_chip",
},
{
type: "source_component",
source_component_id: "cap",
source_group_id: "module",
name: "C_D5",
ftype: "simple_capacitor",
},
{
type: "source_port",
source_port_id: "supply",
source_component_id: "chip",
pin_number: 5,
},
{
type: "source_port",
source_port_id: "positive",
source_component_id: "cap",
pin_number: 1,
},
{
type: "source_port",
source_port_id: "ground",
source_component_id: "cap",
pin_number: 2,
},
{ type: "source_trace", connected_source_port_ids: ["supply", "positive"] },
{ type: "pcb_port", source_port_id: "supply", x: 0, y: 0 },
{ type: "pcb_port", source_port_id: "positive", x: 1, y: 0 },
{ type: "pcb_port", source_port_id: "ground", x: 2, y: 0 },
];
expect(checkCapacitorOrientation(data)).toEqual([]);
data[7].x = 2;
data[8].x = 1;
expect(checkCapacitorOrientation(data)).toHaveLength(1);
data[7].x = data[8].x = 1;
data[7].y = 0.5;
data[8].y = -0.5;
expect(checkCapacitorOrientation(data)).toHaveLength(1);
});