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
antenna-mechanical.ts
/** Ezurio 001-0014 FlexPIFA integration, dimensions in mm.
* Source: https://www.ezurio.com/documentation/datasheet-24-ghz-flexpifa
* Geometry is a dimensioned purchase-part envelope, not an RF simulation.
*/
import { box, cyl, move, turn, union, subtract, tint, type Plan, type V3 } from "./mechanical-primitives"
import { rearHousingDimensions as rear } from "./rear-housing-dimensions"
import type { MechanicalPart } from "./enclosure-geometry"
export const antennaMechanical = {
manufacturer: "Ezurio", partNumber: "001-0014", reference: "ANT1",
source: "https://www.ezurio.com/documentation/datasheet-24-ghz-flexpifa",
purchase: "https://www.digikey.com/en/products/detail/ezurio/001-0014/4732758",
frequencyMHz: [2400, 2480], impedanceOhm: 50, peakGainDbi: 2,
bodyLength: 40.12, width: 11, height: 2.5, overallWidthWithCable: 41.25,
nominalAssemblyLength: 100, assemblyLengthTolerancePlus: 5,
// 100 mm overall in the supplier drawing includes the antenna's 11 mm width.
nominalExposedCablePath: 89, cableDiameterAssumed: 1.13, cableBendRadiusAssumed: 3,
center: [0, 8.5] as [number, number], adhesivePlaneZ: 7, mainElementTopZ: 9.5,
trayBottomZ: 5.5, trayThickness: 1.5,
pocketRoofZ: rear.outerZ, pocketCeilingZ: rear.ceilingZ,
pocketInnerSize: [48, 15.6] as [number, number],
plasticTopClearance: 3, plasticSideClearance: 2, plasticGroundWallClearance: 1,
metalTopClearance: 10, metalSideClearance: 5,
// Espressif Fig 10-2: connector 3 mm from right, 2.46 mm from top.
socketCenter: [6, 19.785, 1.6] as V3,
connectorMatedTopZ: 4.1,
orientation: "Adhesive ground face toward PCB (-Z); main element faces rear (+Z); feed/ground wall at +X",
} as const
const a = antennaMechanical
// Removable dielectric shelf: install ANT1 on it before insertion from the
// open front of the rear cup; two internal M1.6 screws retain its end tabs.
export const antennaTray = subtract(box([54,14,a.trayThickness],[0,8.5,6.25]),
...[-26,26].map(x=>cyl(0.9,5.4,7.1,x,8.5)))
export const antennaTrayLands = union(...[-26,26].map(x=>subtract(
cyl(2.1,7,rear.ceilingZ+0.1,x,8.5),cyl(0.65,6.9,9.1,x,8.5))))
export const antennaTrayPilots = union(...[-26,26].map(x=>cyl(0.65,6.9,9.1,x,8.5)))
const body = union(
tint([0.91,0.59,0.14], box([40.12,11,0.15], [0,8.5,7.075])),
// Simplified folded radiator and ground wall inside the published envelope.
tint([0.72,0.39,0.07], box([40.12,11,0.12], [0,8.5,9.44])),
tint([0.77,0.43,0.09], box([0.15,11,2.3], [19.985,8.5,8.3])),
)
export const antennaBodyEnvelope = box([41.25,11,2.5], [0.565,8.5,8.25])
// Entire body gets the stricter radiator clearance, even where its inactive label is.
export const antennaPlasticKeepout = union(
box([42.12,15,3], [0,8.5,11]),
box([42.12,2,2.5], [0,2,8.25]), box([42.12,2,2.5], [0,15,8.25]),
box([1,11,2.5], [20.56,8.5,8.25]),
)
export const antennaMetalKeepout = box([40.12,21,12.5], [0,8.5,13.25])
// Radius-limited 3D fillets: explicit tangency, no angular kinks in the coax.
function fillet(points: V3[], radius: number): V3[] {
const out: V3[] = [points[0]]
let previousTangent = 0
for (let i=1; i<points.length-1; i++) {
const p=points[i], prev=points[i-1], next=points[i+1]
const u=p.map((n,j)=>prev[j]-n), v=p.map((n,j)=>next[j]-n)
const lu=Math.hypot(...u), lv=Math.hypot(...v)
u.forEach((_,j)=>u[j]/=lu); v.forEach((_,j)=>v[j]/=lv)
const angle=Math.acos(Math.max(-1,Math.min(1,u.reduce((s,n,j)=>s+n*v[j],0))))
const tangent=radius/Math.tan(angle/2)
if(tangent + previousTangent >= lu || tangent >= lv) throw new Error("Coax fillet exceeds segment length")
previousTangent = tangent
const bis=u.map((n,j)=>n+v[j]), lb=Math.hypot(...bis)
const center=p.map((n,j)=>n+bis[j]/lb*radius/Math.sin(angle/2))
const e0=p.map((n,j)=>(n+u[j]*tangent-center[j])/radius)
const end=p.map((n,j)=>(n+v[j]*tangent-center[j])/radius)
const sweep=Math.PI-angle, e1=end.map((n,j)=>(n-e0[j]*Math.cos(sweep))/Math.sin(sweep))
for(let k=0;k<=24;k++) {
const t=sweep*k/24
out.push(center.map((n,j)=>n+radius*(e0[j]*Math.cos(t)+e1[j]*Math.sin(t))) as V3)
}
}
out.push(points[points.length-1])
return out
}
function route(loopY: number): V3[] {
return fillet([
[20.7,3,8.25], [20.7,-4,8.25], [14,loopY,4.6],
[-12,loopY,4.6], [-12,24.5,4.6], [6,24.5,3.55], [6,19.785,3.55],
],a.cableBendRadiusAssumed)
}
const length=(p:V3[])=>p.slice(1).reduce((s,v,i)=>s+Math.hypot(...v.map((n,j)=>n-p[i][j])),0)
let lo=-17, hi=-4
for(let i=0;i<48;i++){const mid=(lo+hi)/2;if(length(route(mid))>a.nominalExposedCablePath)lo=mid;else hi=mid}
export const antennaCablePath=route((lo+hi)/2)
export const antennaCableLength=length(antennaCablePath)
function cableSegment(p:V3,q:V3):Plan {
const d=q.map((n,i)=>n-p[i]), l=Math.hypot(...d)
return move(p.map((n,i)=>(n+q[i])/2) as V3,
turn([0,0,Math.atan2(d[1],d[0])],turn([0,Math.acos(d[2]/l),0],
{type:"cylinder",radius:a.cableDiameterAssumed/2,height:l+0.015,segments:12})))
}
const coax=union(...antennaCablePath.slice(1).map((p,i)=>cableSegment(antennaCablePath[i],p)))
const plug=union(
tint([0.73,0.58,0.2],cyl(1.45,2.75,4.1,6,19.785)),
tint([0.13,0.13,0.14],box([1.2,2.1,0.8],[6,20.5,3.55])),
)
const trayScrew = subtract(union(cyl(1.5,4.3,5.5),cyl(0.8,5.5,7),cyl(0.55,7,8.5)),
box([2,0.4,0.4],[0,0,4.35]),box([0.4,2,0.4],[0,0,4.35]))
export const antennaParts: MechanicalPart[] = [
{id:"antenna-tray",label:"Removable antenna tray · 1.5 mm dielectric",plan:antennaTray,color:[0.27,0.35,0.34],printable:true,group:"controls",explode:[0,0,22],note:"Print for fit; prefer 1.5 mm polycarbonate for RF samples. Peel-and-stick ANT1 first, then insert from open front of rear cup and secure two M1.6 x 3 screws before the PCB."},
...[-26,26].map((x,i):MechanicalPart=>({id:`antenna-tray-screw-${i+1}`,label:"Internal antenna-tray screw · M1.6 x 3",plan:move([x,8.5,0],trayScrew),color:[0.65,0.68,0.7],printable:false,group:"hardware",explode:[0,0,-8],note:"Head faces the PCB; install before PCB. 1.5 mm engagement in blind pilots. Verify purchased screw dimensions and printed pilot fit."})),
{id:"wifi-antenna",label:"ANT1 · Ezurio 001-0014 FlexPIFA",plan:body,color:[0.91,0.59,0.14],printable:false,multicolor:true,group:"reference",explode:[0,0,34],note:"40.12 × 11 × 2.5 mm dimensional reference. Adhesive face on the 1.5 mm dielectric shelf; radiator faces +Z. Antenna is purchased, not printed."},
{id:"wifi-antenna-coax",label:"ANT1 · supplied coax and U.FL plug",plan:union(tint([0.08,0.085,0.09],coax),plug),color:[0.08,0.085,0.09],printable:false,multicolor:true,group:"reference",explode:[0,0,18],note:"Nominal 89 mm exposed route for the supplier's 100 mm overall assembly. 1.13 mm OD and 3 mm bend radius are packaging assumptions; measure the sample, retain slack with nonmetallic tape below the shelf."},
]