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)
  }
}