astra/f1c100s

This code defines and renders a four-layer, top-mounted F1C100S module with detailed pin assignments, schematic annotations, and layout profiles, facilitating design, validation, and routing of the hardware including support for decoupling capacitors, crystal oscillators, and external connectors.

Version
0.11.0
License
unset
Stars
0

src/index.tsx

import type { CircuitJson } from "circuit-json";
import { selectBreakouts } from "./selective-breakouts";
import { joinSavedPathExits } from "./saved-paths";
import { attachSchematicLayout } from "./schematic";
import {
	LAYOUT_PROFILES,
	assertLayoutProfile,
	type LayoutProfile,
} from "./profiles";
import { EXTERNAL_NETS } from "./pin-map";
import native from "./generated/native.circuit.json";
import lcdTopStorageRight from "./generated/lcd_top_storage_right.circuit.json";
import lcdRightStorageBottom from "./generated/lcd_right_storage_bottom.circuit.json";
import lcdTopStorageBottom from "./generated/lcd_top_storage_bottom.circuit.json";
import lcdRightStorageLeft from "./generated/lcd_right_storage_left.circuit.json";

export { LAYOUT_PROFILES, type LayoutProfile };
export {
	F1C100SLcdSchematicBox,
	F1C100SStorageSchematicBox,
	F1C100SGpioSchematicBox,
	F1C100SAudioSchematicBox,
	F1C100SVideoTouchSchematicBox,
	F1C100SSystemSchematicBox,
	F1C100SPowerSchematicBox,
	type F1C100SSchematicBoxProps,
} from "./schematic-boxes";
const profiles: Record<LayoutProfile, unknown> = {
	native,
	lcd_top_storage_right: lcdTopStorageRight,
	lcd_right_storage_bottom: lcdRightStorageBottom,
	lcd_top_storage_bottom: lcdTopStorageBottom,
	lcd_right_storage_left: lcdRightStorageLeft,
};
export interface F1C100SModuleProps {
	name: string;
	layoutProfile?: LayoutProfile;
	pcbX?: number;
	pcbY?: number;
	/** Rotation of the complete stored module, including its exit pads. */
	pcbRotation?: number;
	schX?: number;
	schY?: number;
	schSheetName?: string;
	/** Custom mode lets callers place the exported schematic-box components. */
	schematicLayout?: "default" | "custom";
	/** Only truthy entries create breakout pads and external copper. Internal support is always retained. */
	connections?: Partial<Record<string, string>>;
}

/** A fresh copy prevents one placed instance from mutating another's routes. */
export function getF1C100SCircuitJson(
	layoutProfile: LayoutProfile = "native",
): CircuitJson {
	assertLayoutProfile(layoutProfile);
	return structuredClone(profiles[layoutProfile]) as CircuitJson;
}

/** Inflate the imported components and connectivity only. Copper is loaded
 * separately by the native fanout pcbTracePaths API. */
function prepareForInflation(json: CircuitJson): CircuitJson {
	// Dedicated decouplers have explicit two-port source traces. Their supply
	// connection goes through the assigned U1 pin; only their return joins GND.
	const records = json as any[];
	const processor = records.find(
		(e) => e.type === "source_component" && e.name === "U1",
	);
	for (const cap of records.filter(
		(e) => e.type === "source_component" && /^C_D\d+$/.test(e.name),
	)) {
		const capPort = records.find(
			(e) =>
				e.type === "source_port" &&
				e.source_component_id === cap.source_component_id &&
				e.pin_number === 1,
		);
		const pinPort = records.find(
			(e) =>
				e.type === "source_port" &&
				e.source_component_id === processor.source_component_id &&
				e.pin_number === Number(cap.name.slice(3)),
		);
		const rail = records.find(
			(e) =>
				e.type === "source_trace" &&
				e.connected_source_port_ids.includes(capPort.source_port_id),
		);
		if (
			!rail ||
			!rail.connected_source_port_ids.includes(pinPort.source_port_id)
		)
			throw Error(`Missing decoupling target for ${cap.name}`);
		rail.connected_source_port_ids = rail.connected_source_port_ids.filter(
			(id: string) => id !== capPort.source_port_id,
		);
		records.push({
			type: "source_trace",
			source_trace_id: `source_trace_${cap.name}_direct`,
			name: `D_${cap.name}_U1_pin${pinPort.pin_number}`,
			connected_source_port_ids: [
				capPort.source_port_id,
				pinPort.source_port_id,
			],
			connected_source_net_ids: [],
			subcircuit_id: rail.subcircuit_id,
			min_trace_thickness: 0.12,
			max_length: 1000,
		});
	}
	// Explicit local nets give the schematic conventional named connections.
	// The existing source traces and stored copper retain the same endpoints.
	for (const e of [...json] as any[]) {
		if (e.type !== "source_trace" || !e.name?.startsWith("N_")) continue;
		const netName = e.name.slice(2);
		const id = `source_net_${netName}`;
		(json as any[]).push({
			type: "source_net",
			source_net_id: id,
			name: netName,
			member_source_group_ids: [],
		});
		e.connected_source_net_ids = [id];
	}

	return json
		.filter((e) => e.type !== "pcb_trace" && e.type !== "pcb_via")
		.map((e) =>
			// The pinned inflator lacks crystal and testpoint cases. Temporary
			// chips retain selectors until native components replace them.
			e.type === "source_component" &&
			["simple_crystal", "simple_test_point"].includes(e.ftype)
				? { ...e, ftype: "simple_chip" as const }
				: e,
		) as CircuitJson;
}

/** Four-layer, top-mounted, pre-routed F1C100S + decoupling module.
 * The package uses stored copper. No fanout solver runs during instantiation.
 */
export function F1C100SModule(props: F1C100SModuleProps) {
	if ("busProfile" in props || "variant" in props || "busExits" in props)
		throw new Error("Use layoutProfile to select a stored F1C100S layout");
	const {
		name,
		layoutProfile = "native",
		connections = {},
		schematicLayout = "default",
		...placement
	} = props;
	assertLayoutProfile(layoutProfile);
	if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
		throw new Error("Module name must be a selector-safe identifier");
	for (const port of Object.keys(connections))
		if (!EXTERNAL_NETS.includes(port))
			throw new Error(`Unknown F1C100S terminal '${port}'`);
	const selected = new Set(
		Object.entries(connections)
			.filter(([, target]) => Boolean(target))
			.map(([port]) => port),
	);
	const { circuitJson, paths } = selectBreakouts(
		getF1C100SCircuitJson(layoutProfile),
		selected,
	);
	return (
		<>
			<subcircuit
				name={name}
				{...{
					ref: (instance: Parameters<typeof attachSchematicLayout>[0]) =>
						attachSchematicLayout(
							instance,
							schematicLayout,
							layoutProfile,
							selected,
							paths,
						),
				}}
				minTraceWidth={0.12}
				minViaPadDiameter={0.45}
				minViaHoleDiameter={0.2}
				autorouter={{ local: true, algorithmFn: joinSavedPathExits }}
				schTraceAutoLabelEnabled
				schMaxTraceDistance={3}
				circuitJson={prepareForInflation(circuitJson)}
				{...placement}
			/>
			{Object.entries(connections).map(([port, target]) =>
				target ? (
					<trace
						key={port}
						name={`${name}_${port}_external`}
						from={`.${name} .${port} > .pin1`}
						to={target}
					/>
				) : null,
			)}
		</>
	);
}
export default F1C100SModule;