imrishabh18/rp2040-motor-controller

An RP2040-based NEMA17 stepper-motor controller with USB-C programming and PD motor-power negotiation, DRV8825 dual H-bridge drive, current/temperature telemetry, protected power filtering, status RGB LED, buzzer alarm, and optional magnetic encoder.

Version
1.0.41
License
unset
Stars
3

tests/helpers/assertOuterMotorPower.ts

import type { CircuitJson } from "circuit-json";

/** Recompute motor-net membership, then inspect the actual routed wire layers. */
export function assertOuterMotorPower(json: CircuitJson) {
  const j = json as any[];
  const parent = new Map<string, string>();
  const root = (id: string): string => {
    if (!parent.has(id)) parent.set(id, id);
    if (parent.get(id) !== id) parent.set(id, root(parent.get(id)!));
    return parent.get(id)!;
  };
  for (const e of j) {
    const ids = e.type === "source_trace"
      ? [...e.connected_source_port_ids, ...e.connected_source_net_ids]
      : e.type === "source_component_internal_connection" ? e.source_port_ids : [];
    for (const id of ids.slice(1)) parent.set(root(id), root(ids[0]));
  }
  const port = (name: string, pin: string) => {
    const c = j.find(e => e.type === "source_component" && e.name === name);
    const p = j.find(e => e.type === "source_port" && e.source_component_id === c?.source_component_id &&
      (e.name === pin || e.port_hints?.includes(pin)));
    if (!p) throw new Error(`Missing motor terminal ${name}.${pin}`);
    return p;
  };
  const terminals = [
    ["J_MOTOR_USB", "A4B9"], ["F_MOTOR_VBUS", "pin2"], ["DRIVER", "VMA"],
    ...["AOUT1", "AOUT2", "BOUT1", "BOUT2", "ISENA", "ISENB"].map(p => ["DRIVER", p]),
    ["R_PHASE_A", "pin2"], ["R_PHASE_B", "pin2"],
  ];
  const nets = new Set(terminals.map(([n, p]) => root(port(n!, p!).source_port_id)));
  if (nets.size !== 11) throw new Error("Motor power nets are shorted or missing");
  const sources = new Map(j.filter(e => e.type === "source_trace").map(e => [e.source_trace_id, e]));
  const lengths: Record<string, number> = {top: 0, bottom: 0};
  const checked: string[] = [];
  const checkedNets = new Set<string>();
  for (const t of j.filter(e => e.type === "pcb_trace")) {
    const s = sources.get(t.source_trace_id);
    const ids = s ? [...s.connected_source_port_ids, ...s.connected_source_net_ids] : t.source_net_id ? [t.source_net_id] : [];
    if (!ids.length || !nets.has(root(ids[0]))) continue;
    checked.push(t.pcb_trace_id); checkedNets.add(root(ids[0]));
    for (const p of t.route) {
      if (p.route_type === "wire" && !["top", "bottom"].includes(p.layer)) {
        throw new Error(`Motor-current copper on inner layer: ${s?.name ?? t.pcb_trace_id} / ${p.layer}`);
      }
      if (p.route_type === "through_pad" &&
        (!["top", "bottom"].includes(p.start_layer) || !["top", "bottom"].includes(p.end_layer))) {
        throw new Error(`Motor-current path enters inner copper at plated pad: ${t.pcb_trace_id}`);
      }
    }
    for (let i = 1; i < t.route.length; i++) {
      const a=t.route[i-1], b=t.route[i];
      if (a.route_type === "wire" && b.route_type === "wire" && a.layer === b.layer) {
        lengths[a.layer]! += Math.hypot(b.x-a.x, b.y-a.y);
      }
    }
  }
  if (checkedNets.size !== nets.size) throw new Error("Motor power nets are missing routed copper");
  return {status: "pass", motorNets: nets.size, checkedTraceCount: checked.length, checkedTraceIds: checked,
    wireLengthMmByLayer: lengths, allowedWireLayers: ["top", "bottom"], throughViasAllowed: true};
}