shibosoftwaredev/f1c100s-linux-nema17-stepper-controller
Compact four-layer NEMA 17 carrier PCB combining an F1C100S Linux SoC, USB-C PD power and buck regulation, W5500 10/100 Ethernet, STM32 safety/motion control, and DRV8825 stepper-driver hardware with protection, sensing, connectors, and dual-sided SMT assembly.
- Version
- 0.4.15
- License
- unset
- Stars
- 0
modules/f1c100s/src/index.tsx
import type { CircuitJson } from "circuit-json";
import { Fragment } from "react";
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>>;
/** Optional XY offsets for selected breakout test points, applied before the
* stored fanout is emitted. This lets a carrier clear a local obstruction
* without moving the processor module or changing its external nets. */
breakoutOffsets?: Partial<Record<string, { x: number; y: number }>>;
}
/** 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[];
// The recessed Type-C shells occupy four north-edge support locations.
// Keep only those passives on the motor-side face; every other module part
// retains the original, already-validated top-side fanout.
const motorSideSupportNames = new Set([
"C_B_VCC_DRAM", "C_VRA2", "R_VRA2", "C_B_VCC_TV", "C_TV_REF", "R_PU_SPI0_CS",
]);
const motorSideSourceIds = new Set(records
.filter((e) => e.type === "source_component" && motorSideSupportNames.has(e.name))
.map((e) => e.source_component_id));
const motorSidePcbIds = new Set(records
.filter((e) => e.type === "pcb_component" && motorSideSourceIds.has(e.source_component_id))
.map((e) => e.pcb_component_id));
for (const e of records) {
if (e.type === "pcb_component" && motorSidePcbIds.has(e.pcb_component_id)) e.layer = "bottom";
else if (motorSidePcbIds.has(e.pcb_component_id) && "layer" in e) e.layer = "bottom";
}
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 = {},
breakoutOffsets = {},
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,
);
for (const [breakoutName, offset] of Object.entries(breakoutOffsets)) {
if (!offset || !selected.has(breakoutName)) continue;
const records = circuitJson as any[];
const source = records.find(
(e) => e.type === "source_component" && e.name === breakoutName,
);
const component = records.find(
(e) => e.type === "pcb_component" && e.source_component_id === source?.source_component_id,
);
if (!component) throw new Error(`Missing selected breakout '${breakoutName}'`);
const originalCenter = { ...component.center };
component.center = {
x: originalCenter.x + offset.x,
y: originalCenter.y + offset.y,
};
if (Number.isFinite(component.display_offset_x))
component.display_offset_x += offset.x;
if (Number.isFinite(component.display_offset_y))
component.display_offset_y += offset.y;
for (const record of records.filter(
(e) => e.pcb_component_id === component.pcb_component_id && e !== component,
)) {
if (Number.isFinite(record.x)) record.x += offset.x;
if (Number.isFinite(record.y)) record.y += offset.y;
if (record.center) record.center = {
x: record.center.x + offset.x,
y: record.center.y + offset.y,
};
if (record.outline) record.outline = record.outline.map((point: any) => ({
...point, x: point.x + offset.x, y: point.y + offset.y,
}));
}
let adjustedPaths = 0;
for (const path of paths) {
if (!path.route.length) continue;
const route = path.route as any[];
const first = route[0];
const last = route.at(-1);
const atOriginal = (point: any) =>
Math.abs(point.x - originalCenter.x) < 1e-5 &&
Math.abs(point.y - originalCenter.y) < 1e-5;
if (atOriginal(first)) {
path.route = [
{ ...first, x: first.x + offset.x, y: first.y + offset.y },
{ ...first },
...route.slice(1),
] as any;
adjustedPaths++;
} else if (atOriginal(last)) {
path.route = [
...route,
{ ...last, x: last.x + offset.x, y: last.y + offset.y },
] as any;
adjustedPaths++;
}
}
if (!adjustedPaths)
throw new Error(`Missing saved fanout endpoint for breakout '${breakoutName}'`);
}
return (
<>
<subcircuit
name={name}
{...{
ref: (instance: Parameters<typeof attachSchematicLayout>[0]) =>
attachSchematicLayout(
instance,
schematicLayout,
layoutProfile,
selected,
paths,
breakoutOffsets,
),
}}
minTraceWidth={0.12}
minViaPadDiameter={0.45}
minViaHoleDiameter={0.3}
autorouter={{ local: true, algorithmFn: joinSavedPathExits }}
schTraceAutoLabelEnabled
schMaxTraceDistance={3}
circuitJson={prepareForInflation(circuitJson)}
{...placement}
/>
{Object.entries(connections).map(([port, target]) =>
target ? (
<Fragment key={port}>
<trace
name={`${name}_${port}_external`}
from={`.${name} .${port} > .pin1`}
to={target}
/>
</Fragment>
) : null,
)}
</>
);
}
export default F1C100SModule;