mohan-bee/flight-controller-v2

A 64 mm STM32F405 flight-controller PCB with ICM-42688 IMU, SPI flash, USB-C/ESD, battery buck and 3.3 V regulation, four motor outputs, service I/O, and PETG standoff mounting hardware.

Version
0.1.0
License
ISC
Stars
0

tools/audit-mount.mjs

import assert from "node:assert/strict";
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
import modeling from "@jscad/modeling";
import { executeJscadOperations } from "jscad-planner";
import { mountingPoints } from "../assembly/mount";

const standalone = JSON.parse(readFileSync("dist/index/circuit.json", "utf8"));
const assembled = JSON.parse(readFileSync("dist/assembly/circuit.json", "utf8"));
const physicalTypes = ["pcb_board", "pcb_component", "pcb_port", "pcb_smtpad",
  "pcb_plated_hole", "pcb_hole", "pcb_trace", "pcb_via", "pcb_copper_pour",
  "pcb_cutout", "pcb_solder_paste", "pcb_solder_mask"];
function physicalGeometry(element) {
  if (Array.isArray(element)) return element.map(physicalGeometry);
  if (element && typeof element === "object") {
    return Object.fromEntries(Object.entries(element)
      .filter(([key]) => !key.endsWith("_id") && !key.endsWith("_ids") &&
        !["name", "subcircuit_connectivity_map_key"].includes(key))
      .sort(([left], [right]) => left.localeCompare(right))
      .map(([key, child]) => [key, physicalGeometry(child)]));
  }
  return element;
}
const counts = {};
for (const type of physicalTypes) {
  const before = standalone.filter(e => e.type === type).map(e => JSON.stringify(physicalGeometry(e))).sort();
  const after = assembled.filter(e => e.type === type).map(e => JSON.stringify(physicalGeometry(e))).sort();
  assert.deepEqual(after, before, `Assembly changed ${type} geometry`);
  counts[type] = after.length;
}
assert(!assembled.some(e => e.type.endsWith("_error")), "Assembly has circuit errors");
const source = assembled.find(e => e.type === "source_component" && e.name === "MOUNT");
const mount = assembled.find(e => e.type === "cad_component" && e.source_component_id === source.source_component_id);
assert.deepEqual(mount.position, { x: 0, y: 0, z: -12.8 });
assert.deepEqual(mount.rotation, { x: 0, y: 0, z: 0 });
const localSolid = executeJscadOperations(modeling, mount.model_jscad);
const solid = modeling.transforms.translate([mount.position.x, mount.position.y, mount.position.z], localSolid);
const [minimum, maximum] = modeling.measurements.measureBoundingBox(solid);
assert(Math.abs(minimum[2] + 12.8) < 1e-6 && Math.abs(maximum[2] + 0.8) < 1e-6);
for (const { x, y } of mountingPoints) {
  assert(assembled.some(e => e.type === "pcb_hole" && e.x === x && e.y === y && e.hole_diameter === 3.2));
  const bore = modeling.primitives.cylinder({ radius: 1.65, height: 16, center: [x, y, -6.8] });
  assert(Math.abs(modeling.measurements.measureVolume(modeling.booleans.intersect(solid, bore))) < 1e-6,
    "Printed bore does not align with the emitted PCB hole");
  const support = modeling.booleans.subtract(
    modeling.primitives.cylinder({ radius: 2.9, height: 0.05, center: [x, y, -0.825] }),
    modeling.primitives.cylinder({ radius: 1.8, height: 1, center: [x, y, -0.825] }),
  );
  assert(modeling.measurements.measureVolume(modeling.booleans.intersect(solid, support)) > 0.7,
    "Post does not support the board underside");
}
const mechanicalCad = assembled.filter(e => e.type === "cad_component" && e.model_jscad);
assert.equal(mechanicalCad.length, 13, "Expected base and four screw/washer/nut sets");

// Export the same native CAD plan, in its printable local frame (base at Z=0).
const triangulated = modeling.modifiers.generalize({ snap: true, triangulate: true }, localSolid);
const facets = modeling.geometries.geom3.toPolygons(triangulated).map(polygon => {
  const [a, b, c] = polygon.vertices;
  assert.equal(polygon.vertices.length, 3);
  const normal = modeling.maths.vec3.normalize([], modeling.maths.vec3.cross([],
    modeling.maths.vec3.subtract([], b, a), modeling.maths.vec3.subtract([], c, a)));
  return `facet normal ${normal.join(" ")}\nouter loop\n${polygon.vertices.map(v => `vertex ${v.join(" ")}`).join("\n")}\nendloop\nendfacet`;
});
mkdirSync("mount", { recursive: true });
writeFileSync("mount/fc-v2-mount.stl", `solid fc_v2_mount\n${facets.join("\n")}\nendsolid fc_v2_mount\n`);
const report = { status: "pass", pcb_geometry_unchanged: counts, mounting_pattern_mm: 56,
  mounting_holes: 4, printed_bore_diameter_mm: 3.4, board_underside_z_mm: -0.8,
  printed_part_bounds_mm: { minimum, maximum }, underside_to_base_clearance_mm: 10,
  nominal_hardware_sets: 4, stl_triangles: facets.length,
  scope: "Native assembly geometry and hole alignment only. Prototype mount and nominal hardware; threads, vibration, flight loads and print tolerances are not qualified." };
writeFileSync("validation/mount-assembly.json", JSON.stringify(report, null, 2) + "\n");
console.log(JSON.stringify(report, null, 2));