muse/book-reading-clip-lamp

A Wi‑Fi/USB‑C ESP32‑S3 reading lamp with OV5640 camera, microphone, speaker amplifier, rechargeable Li‑Po power management, six-LED boost light, USB protection/charging detection, and a clip/gooseneck enclosure with external FlexPIFA antenna.

Version
0.4.3
License
unset
Stars
0

verify-pcb-mounting.ts

import { readFileSync, writeFileSync } from "node:fs"
import { pcbMounting } from "./pcb-mounting"
import { mechanical } from "./enclosure-geometry"
const circuit = JSON.parse(readFileSync("dist/index/circuit.json", "utf8"))
const geometry = JSON.parse(readFileSync("mechanical/geometry-checks.json", "utf8"))
const assert = (condition: boolean, message: string) => { if (!condition) throw new Error(message) }
assert(JSON.stringify(mechanical.boardMounts) === JSON.stringify(pcbMounting.centers), "PCB/enclosure axes differ")
assert(!geometry.intersections.length && !geometry.boardIntersections.length, "Assembly has physical interference")
assert(geometry.boardFastenerChecks?.length === 4 && geometry.boardFastenerChecks.every((p:any)=>p.supportIntact && p.pilotClear && p.bezelHeadClear), "Support/pilot/head geometry check failed")
const distance = (x:number,y:number,a:any,b:any) => {
  const dx=b.x-a.x,dy=b.y-a.y,dd=dx*dx+dy*dy
  const t=dd?Math.max(0,Math.min(1,((x-a.x)*dx+(y-a.y)*dy)/dd)):0
  return Math.hypot(x-a.x-t*dx,y-a.y-t*dy)
}
const reports = pcbMounting.centers.map(([x,y],i) => {
  const holes=circuit.filter((p:any)=>p.type==="pcb_hole" && Math.hypot(p.x-x,p.y-y)<1e-8)
  assert(holes.length===1 && holes[0].hole_shape==="circle" && holes[0].hole_diameter===pcbMounting.clearanceDiameter, `Mount ${i+1}: missing/wrong NPTH drill`)
  for(const layer of ["top","bottom"]){
    const keepouts=circuit.filter((p:any)=>p.type==="pcb_keepout" && p.shape==="circle" && p.layers.includes(layer) && Math.hypot(p.center.x-x,p.center.y-y)<1e-8)
    assert(keepouts.length===1 && keepouts[0].radius===pcbMounting.surfaceKeepoutRadius, `Mount ${i+1}: missing ${layer} hardware keepout`)
  }
  let surfaceCopperRadius=Infinity, innerCopperRadius=Infinity
  for(const trace of circuit.filter((p:any)=>p.type==="pcb_trace")){
    for(let j=1;j<trace.route.length;j++){
      const a=trace.route[j-1],b=trace.route[j]
      if(a.route_type!=="wire"||b.route_type!=="wire"||a.layer!==b.layer)continue
      const r=distance(x,y,a,b)-Math.max(a.width,b.width)/2
      if(["top","bottom"].includes(a.layer))surfaceCopperRadius=Math.min(surfaceCopperRadius,r)
      else innerCopperRadius=Math.min(innerCopperRadius,r)
    }
  }
  for(const via of circuit.filter((p:any)=>p.type==="pcb_via"))surfaceCopperRadius=Math.min(surfaceCopperRadius,Math.hypot(via.x-x,via.y-y)-via.outer_diameter/2)
  assert(surfaceCopperRadius>=pcbMounting.surfaceKeepoutRadius-1e-6, `Mount ${i+1}: trace/via under mounting hardware`)
  assert(innerCopperRadius>=pcbMounting.clearanceDiameter/2+0.2-1e-6, `Mount ${i+1}: internal trace too close to drill`)
  return {center:[x,y],npthDiameter:holes[0].hole_diameter,surfaceCopperRadius,innerCopperRadius}
})
writeFileSync("mechanical/pcb-mounting-checks.json",JSON.stringify({screw:"M3 x 6 button head",mounts:reports,physicalQualificationRequired:true},null,2)+"\n")
console.log("PASS: four M3 NPTH drills match enclosure axes; exposed copper clears screw heads/posts; inner traces clear drills; assembly support/pilot/head checks pass")