shibosoftwaredev/f1c100s-linux-nema17-stepper-controller
A four-layer NEMA 17 motor-controller PCB combining an Allwinner F1C100S Linux computer, USB‑PD/buck power system, W5500 Ethernet, STM32 real-time motion control, DRV8825 stepper driver, protection/temperature/current telemetry, USB interfaces, and local LED/buzzer diagnostics.
- Version
- 0.4.56
- License
- unset
- Stars
- 0
board-layout.ts
import { repositionPcbComponentTo, transformPcbElements } from "@tscircuit/circuit-json-util"
import { rotateDEG } from "transformation-matrix"
import plan from "./placement-plan.json"
import refinements from "./placement-refinements.json"
import factoryProbes from "./factory-probe-placement.json"
import localBias from "./local-bias-placement.json"
import currentSenseRelayout from "./current-sense-relayout.json"
import traceConstraints from "./routing/trace-constraints.json"
import reviewFixes from "./review-placement-fixes.json"
import usbAttachPlacement from "./usb-attach-placement.json"
import round8Placement from "./round8-placement.json"
import round9Placement from "./round9-placement.json"
import usbTopPlacement from "./usb-top-placement.json"
import reviewPlacement from "./review-placement.json"
import softwareCurrentPlacement from "./software-current-placement.json"
type Placement = { x: number; y: number; rotation: number; layer: "top" | "bottom" }
const placements = Object.fromEntries(Object.entries({ ...plan, ...currentSenseRelayout, ...usbAttachPlacement, ...round8Placement, ...round9Placement, ...softwareCurrentPlacement }).map(([name, placement]) =>
[name, { ...placement, ...(refinements as Record<string, Partial<Placement>>)[name],
...(factoryProbes as Record<string, Partial<Placement>>)[name],
...(localBias as Record<string, Partial<Placement>>)[name],
...(currentSenseRelayout as Record<string, Partial<Placement>>)[name],
...(reviewFixes as Record<string, Partial<Placement>>)[name],
...(usbAttachPlacement as Record<string, Partial<Placement>>)[name],
...(round8Placement as Record<string, Partial<Placement>>)[name],
...(round9Placement as Record<string, Partial<Placement>>)[name],
...(usbTopPlacement as Record<string, Partial<Placement>>)[name],
...(reviewPlacement as Record<string, Partial<Placement>>)[name],
...(softwareCurrentPlacement as Record<string, Partial<Placement>>)[name] }])) as Record<string, Placement>
export function getBoardPlacement(name: string) {
const placement = placements[name]
if (!placement) throw new Error(`Missing placement for ${name}`)
return placement
}
/** Apply assembly faces before footprints exist. Routing is generated only
* after every component and mechanical feature reaches its final position. */
export function prepareBoardPlacement(board: any) {
const visit = (node: any) => {
const placement = placements[node.props?.name]
if (node._parsedProps) {
if (node.componentName === "Trace") {
node._parsedProps.pcbPath = undefined
node._parsedProps.pcbStraightLine = undefined
node._parsedProps.pcbRouteHints = undefined
node._parsedProps.autoroutingPhase = undefined
} else if (placement && !node.pcb_component_id) {
// Core's native CAD renderer reads props.layer, while footprints read
// _parsedProps.layer. Keep both inputs consistent before either runs.
node.props.layer = placement.layer
node._parsedProps.layer = placement.layer
// Inflated SoC geometry carries its own rotation. Only native JSX
// components need their orientation supplied before footprint render.
if (!node._isInflatedFromCircuitJson) {
node.props.pcbRotation = placement.rotation
node._parsedProps.pcbRotation = placement.rotation
}
}
}
for (const child of node.children ?? []) visit(child)
}
visit(board)
}
export function finishBoardPlacement(board: any) {
if (board.__placementApplied) return
board.__placementApplied = true
const db = board.root.db
const json = db.toArray()
for (const pcb of db.pcb_component.list()) {
const source = db.source_component.get(pcb.source_component_id)
const placement = placements[source?.name]
if (!placement) throw new Error(`Missing placement for ${source?.name}`)
const delta = placement.rotation - pcb.rotation
if (Math.abs(delta) > 1e-8) {
const records = json.filter((element: any) =>
element === pcb || element.pcb_component_id === pcb.pcb_component_id)
transformPcbElements(records, rotateDEG(delta, pcb.center.x, pcb.center.y))
}
repositionPcbComponentTo(json, pcb.pcb_component_id, placement)
}
// The imported crystal has real pads but no courtyard. Reserve its pad
// envelope plus 0.15 mm assembly allowance on each side, and let ordinary
// placement DRC reject any collision with that envelope.
const crystalSource = db.source_component.list().find((part: any) => part.name === "Y1")
const crystal = db.pcb_component.getWhere({source_component_id: crystalSource?.source_component_id})
if (crystal && !db.pcb_courtyard_rect.list().some((item: any) => item.pcb_component_id === crystal.pcb_component_id))
db.pcb_courtyard_rect.insert({pcb_component_id:crystal.pcb_component_id,
center:crystal.center,width:crystal.width+.3,height:crystal.height+.3,
ccw_rotation:0,layer:crystal.layer,subcircuit_id:crystal.subcircuit_id})
// Some imported pin attributes retain a top-only hint even after their
// footprint is placed on the rear face. Physical pad copper is authoritative
// for layer access; through-hole barrels remain accessible on every layer.
const access = new Map<string, Set<string>>()
for (const primitive of [...db.pcb_smtpad.list(), ...db.pcb_plated_hole.list()]) {
if (!primitive.pcb_port_id) continue
const layers = access.get(primitive.pcb_port_id) ?? new Set<string>()
for (const layer of primitive.layers ?? [primitive.layer]) layers.add(layer)
access.set(primitive.pcb_port_id, layers)
}
for (const [id, layers] of access) db.pcb_port.update(id, { layers: [...layers] })
// Preserve the working board's electrical routing budgets, including its
// two allowed W5500 oscillator layer transitions and 15 mm clock limit.
for (const [id, constraint] of Object.entries(traceConstraints as Record<string, {
name?: string; max_length?: number; max_via_count?: number; min_trace_thickness?: number
}>)) {
const source = db.source_trace.get(id)
if (!source || source.name !== constraint.name) throw new Error(`Changed trace identity ${id}`)
db.source_trace.update(id, constraint)
}
}