seveibar/pd-dual-stepper-controller

Bottom-side PCB silkscreen artwork consisting of embedded mirrored TS Circuit SVG logos, with via-clearance-aware lower-board placement.

Version
0.1.0
License
unset
Stars
0

tests/dual.test.ts

import { expect, test } from "bun:test";
import { readFileSync } from "node:fs";
const json: any[] = JSON.parse(readFileSync(process.env.CIRCUIT_JSON_PATH ?? "dist/dual/capture.json", "utf8"));
const components = json.filter(e=>e.type==="source_component");
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]));}
function net(name:string,pin:string){const c=components.find(e=>e.name===name);if(!c)throw Error(`Missing ${name}`);const p=json.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 Error(`Missing ${name}.${pin}`);return root(p.source_port_id);}
const same=(a:string,b:string,c:string,d:string)=>expect(net(a,b)).toBe(net(c,d));
test("two independent DRV8825 bridges and eight distinct motor outputs",()=>{
 expect(components.filter(e=>e.manufacturer_part_number==="DRV8825PWPR")).toHaveLength(2);
 const outputs=["DRIVER","DRIVER_M2"].flatMap(d=>["AOUT1","AOUT2","BOUT1","BOUT2"].map(p=>net(d,p)));
 expect(new Set(outputs).size).toBe(8);
 for(let i=0;i<4;i++)same("DRIVER_M2",["AOUT1","AOUT2","BOUT1","BOUT2"][i],"J2",`pin${i+1}`);
 same("DRIVER","AOUT2","J1","pin2");same("DRIVER","BOUT2","J1","pin4");
 same("R_PHASE_A","pin2","J1","pin1");same("R_PHASE_B","pin2","J1","pin3");
});
test("independent motion GPIOs, reset-safe enable and PD-gated sleep",()=>{
 for(const [gpio,pin] of [[8,"STEP"],[9,"DIR"],[10,"nRESET"],[11,"nENBL"],[13,"nFault"]] as const)same("U1",`GPIO${gpio}`,"DRIVER_M2",pin);
 for(const pin of ["STEP","DIR","nENBL","nRESET","nSleep","nFault"])expect(net("DRIVER",pin)).not.toBe(net("DRIVER_M2",pin));
 same("U1","GPIO12","Q_PD_ENABLE_M2","source");same("U_PD","PG","Q_PD_ENABLE_M2","gate");same("Q_PD_ENABLE_M2","drain","DRIVER_M2","nSleep");
 same("R_M2_REQUEST_PD","pin1","Q_PD_ENABLE_M2","source");same("R_M2_REQUEST_PD","pin2","DRIVER_M2","EP");
 same("R_DRV_ENABLE_PU_M2","pin1","DRIVER_M2","nENBL");same("R_DRV_ENABLE_PU_M2","pin2","L_AVDD","pin1");
});
test("common protected motor supply, ground, and separate current sense networks",()=>{
 for(const d of ["DRIVER","DRIVER_M2"]){same(d,"VMA","D_MOTOR_BLOCK","cathode");same(d,"VMB","D_MOTOR_BLOCK","cathode");for(const p of ["EP","GND1","GND2","MODE0","MODE1"])same(d,p,"U_PD","GND");expect(net(d,"V3P3OUT")).not.toBe(net("L_AVDD","pin1"));}
 for(const suffix of ["","_M2"]){for(const p of ["A","B"]){same(`DRIVER${suffix}`,`${p}ISEN`,`R_ISEN_${p}${suffix}`,"pin1");expect(net(`R_ISEN_${p}${suffix}`,"pin1")).not.toBe(net("U_PD","GND"));same(`R_ISEN_${p}${suffix}`,"pin2","U_PD","GND");}same(`DRIVER${suffix}`,"MODE2",`DRIVER${suffix}`,"V3P3OUT");}
 expect(net("DRIVER","V3P3OUT")).not.toBe(net("DRIVER_M2","V3P3OUT"));
 expect(net("J_MOTOR_USB","A4B9")).not.toBe(net("J_USB","A4B9"));
});
test("55 x 45 mm board with four M3 clearance holes and screw keepouts",()=>{
 const b=json.find(e=>e.type==="pcb_board");expect(b.width).toBe(55);expect(b.height).toBe(45);
 const holes=json.filter(e=>e.type==="pcb_hole"&&Math.abs((e.hole_diameter??e.diameter??0)-3.3)<.001);
 expect(holes).toHaveLength(4);expect(holes.map(e=>[e.x,e.y]).sort()).toEqual([[-21,-20],[-21,17],[26,-20],[26,17]].sort());
 expect(json.filter(e=>e.type==="pcb_keepout"&&e.radius===3.2)).toHaveLength(4);
 expect(components.some(e=>e.name==="U_ENCODER")).toBe(false);
});
test("no source creation or schematic electrical errors",()=>{
 expect(json.filter(e=>e.type.startsWith("source_")&&e.type.endsWith("_error"))).toEqual([]);
});

test("both motor connectors are right-angle JST PH and face the right edge",()=>{
 for(const name of ["J1","J2"]){
  const c=components.find(e=>e.name===name);
  expect(c.manufacturer_part_number).toBe("S4B-PH-K-S(LF)(SN)");
  const p=json.find(e=>e.type==="pcb_component"&&e.source_component_id===c.source_component_id);
  expect(p.rotation).toBe(90);
  expect(p.insertion_direction).toBe("from_right");
  expect(p.center.x+6.25).toBe(30);
 }
});