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.42
License
unset
Stars
3

tests/feedback.test.tsx

import { expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { Circuit } from "tscircuit";
import Board from "../index.circuit";

test("optional bottom encoder, buzzer switching/flyback and low-profile motor capacitor", async () => {
  let json: any[];
  if (process.env.CIRCUIT_JSON_PATH) json = JSON.parse(readFileSync(process.env.CIRCUIT_JSON_PATH, "utf8"));
  else {
    const circuit = new Circuit({ platform: { routingDisabled: true } });
    circuit.add(<Board routingDisabled />);
    await circuit.renderUntilSettled();
    json = circuit.getCircuitJson();
  }
  const bottomComponents = json.filter(e => e.type === "pcb_component" && e.layer === "bottom" &&
    json.some(s => s.type === "source_component" && s.source_component_id === e.source_component_id));
  expect(bottomComponents).toHaveLength(3);
  const encoder = json.find(e => e.type === "source_component" && e.name === "U_ENCODER");
  const bottomNames = bottomComponents.map(c => json.find(s => s.type === "source_component" && s.source_component_id === c.source_component_id).name);
  expect(new Set(bottomNames)).toEqual(new Set(["U_ENCODER", "C_ENCODER", "C_ENCODER_BULK"]));
  expect(bottomComponents.every(c => c.do_not_place)).toBe(true);
  expect(bottomComponents.find(c => c.source_component_id === encoder.source_component_id).center).toEqual({ x: 0, y: 0 });
  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 json) {
    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 component = (name: string) => json.find(e => e.type === "source_component" && e.name === name)!;
  const pin = (name: string, label: string) => {
    const port = json.find(e => e.type === "source_port" && e.source_component_id === component(name).source_component_id &&
      (e.name === label || e.port_hints?.includes(label)));
    if (!port) throw new Error(`Missing ${name}.${label}`);
    return root(port.source_port_id);
  };
  // Physical package pins, independently checked against the imported datasheets.
  const groups = [
    [["U1", "GPIO0"], ["U_ENCODER", "pin6"], ["R_ENC_SDA", "pin1"]],
    [["U1", "GPIO1"], ["U_ENCODER", "pin7"], ["R_ENC_SCL", "pin1"]],
    [["U_ENCODER", "pin1"], ["U_ENCODER", "pin2"], ["C_ENCODER", "pin1"], ["C_ENCODER_BULK", "pin1"], ["R_ENC_SDA", "pin2"], ["R_ENC_SCL", "pin2"], ["L_AVDD", "pin1"], ["BZ1", "pin1"], ["D_BUZZER", "cathode"], ["C_BUZZER", "pin1"]],
    [["U_ENCODER", "pin4"], ["U_ENCODER", "pin8"], ["C_ENCODER", "pin2"], ["C_ENCODER_BULK", "pin2"], ["U1", "GND"], ["Q_BUZZER", "pin2"],
      ["R_BUZZER_PD", "pin2"], ["C_BUZZER", "pin2"], ["C_MOTOR_BULK", "pin2"]],
    [["U1", "GPIO16"], ["R_BUZZER_GATE", "pin1"]],
    [["R_BUZZER_GATE", "pin2"], ["Q_BUZZER", "pin1"], ["R_BUZZER_PD", "pin1"]],
    [["Q_BUZZER", "pin3"], ["BZ1", "pin2"], ["D_BUZZER", "anode"]],
    [["D_MOTOR_BLOCK", "cathode"], ["C_MOTOR_BULK", "pin1"]],
  ];
  for (const group of groups) for (const p of group.slice(1)) expect(pin(p[0]!, p[1]!)).toBe(pin(group[0]![0]!, group[0]![1]!));
  expect(new Set(groups.map(g => pin(g[0]![0]!, g[0]![1]!))).size).toBe(groups.length);
  expect(component("R_BUZZER_GATE").resistance).toBe(100);
  expect(component("R_BUZZER_PD").resistance).toBe(100000);
  for (const [name, mpn] of [["BZ1", "HYG-8503A"], ["C_MOTOR_BULK", "TCJD226M050R0090E"]]) {
    expect(component(name!).manufacturer_part_number).toBe(mpn);
    const pcb = json.find(e => e.type === "pcb_component" && e.source_component_id === component(name!).source_component_id)!;
    expect(pcb.layer).toBe("top");
    const cad = json.find(e => e.type === "cad_component" && e.pcb_component_id === pcb.pcb_component_id)!;
    expect(cad.model_obj_url).toContain("modelcdn.tscircuit.com");
    if (name === "C_MOTOR_BULK") {
      expect(component(name).capacitance).toBeCloseTo(22e-6, 8);
      expect(component(name).supplier_part_numbers.jlcpcb).toEqual(["C5981428"]);
      expect(component(name).max_voltage_rating).toBe(50);
      expect(component(name).are_pins_interchangeable).toBe(false);
      const positive = json.find(e => e.type === "source_port" && e.source_component_id === component(name).source_component_id && e.name === "pin1");
      const positivePad = json.find(e => e.type === "pcb_port" && e.source_port_id === positive.source_port_id);
      // Supplier D-case anode is on the lower pad at this 90° rotation.
      expect(positivePad.x).toBeCloseTo(-18, 6);
      expect(positivePad.y).toBeCloseTo(-4.852419, 6);
      expect(json.filter(e => e.type === "pcb_smtpad" && e.pcb_component_id === pcb.pcb_component_id)).toHaveLength(2);
      expect(json.filter(e => e.type === "pcb_plated_hole" && e.pcb_component_id === pcb.pcb_component_id)).toHaveLength(0);
    }
  }
}, 60_000);