Devesh36/pocket-audio-player

A portable, rechargeable microSD audio player built around an RP2040, SPI flash, USB‑C charging/power management, LCD/button controls, PCM5102A I²S DAC, and TPA6132A headphone amplifier in a 3D‑printed enclosure.

Version
1.0.12
License
unset
Stars
0

enclosure.tsx

import { Fragment } from "react"
import { assembly, jscad } from "tscircuit"
import { oledMounts } from "./oled-shape"
import { oledModelUrl, oledExplodedModelUrl } from "./oled-model"
import models from "./mechanical-models.json"

// Millimetres; the existing routed PCB remains at Z=0.
export const caseDimensions={width:64,height:110,centerY:-7,floorZ:-7.5,
 floorThickness:1.0,pcbBottomZ:-0.8,seamZ:7.4,lidBottomZ:9.4,lidTopZ:10.6,
 wallThickness:1.5,lipClearance:0.3,
 displayWindow:{x:0,y:17.2,width:43.72,height:23.7}}
export const batteryDimensions={width:29,height:36,thickness:4.75,center:[0,-29,-3.5] as const,
 capacityMah:500,nominalVoltage:3.7,supplier:"Adafruit 1578",chargeCurrentMa:100}
export const mountingHoles=[-26,26].flatMap(x=>[-41,41].map(y=>[x,y] as const))
export const lidMagnets=[-28,28].flatMap(x=>[-57,43].map(y=>[x,y] as const))
// Mechanical positions are unchanged. Firmware assigns navigation functions.
export const controlButtons=[['SELECT',0,-27],['PREV',-12,-27],['NEXT',12,-27],
 ['MENU',0,-15],['PLAY_PAUSE',0,-39]] as const
export const ringAngles={MENU:90,PREV:180,NEXT:0,PLAY_PAUSE:270} as const

export function ShellSolid({width,height,depth,z,radius=5,centerY=-7}: {
 width:number;height:number;depth:number;z:number;radius?:number;centerY?:number}) {
 const centers=[[width/2-radius,height/2-radius],[-width/2+radius,height/2-radius],
 [-width/2+radius,-height/2+radius],[width/2-radius,-height/2+radius]]
 const points: [number,number][]=centers.flatMap(([x,y],corner)=>Array.from({length:13},(_,i)=>{
  const a=(corner+i/12)*Math.PI/2;return [x+radius*Math.cos(a),y+radius*Math.sin(a)] as [number,number]
 }))
 return <jscad.translate offset={[0,centerY,z]}><jscad.extrudeLinear height={depth}>
  <jscad.polygon points={points}/></jscad.extrudeLinear></jscad.translate>
}
function PortCuts(){return <Fragment>
 <jscad.cuboid size={[14,8,7]} center={[-7,47,3]}/>
 <jscad.rotate angles={[Math.PI/2,0,0]}><jscad.cylinder radius={5.5} height={9} center={[20,3.8,-46.5]}/></jscad.rotate>
 <jscad.cuboid size={[9,18,4.2]} center={[-32,15,1.8]}/>
</Fragment>}
export function CaseBase({drop=0}: {drop?:number}){return <Fragment>
 <jscad.translate offset={[0,0,drop]}><jscad.colorize color="#c6cdd1"><jscad.subtract>
  <jscad.union>
   <jscad.subtract>
    <ShellSolid width={64} height={110} depth={14.9} z={-7.5}/>
    <ShellSolid width={61} height={106} depth={21} z={-6.5} radius={2}/>
   </jscad.subtract>
   <jscad.subtract>
    <ShellSolid width={64} height={110} depth={0.3} z={7.1}/>
    <ShellSolid width={58.4} height={102.4} depth={0.6} z={7} radius={1}/>
   </jscad.subtract>
   <jscad.subtract>
    <ShellSolid width={60.4} height={105.4} depth={2.1} z={7.3} radius={1.7}/>
    <ShellSolid width={58.4} height={102.4} depth={2.5} z={7.2} radius={1}/>
   </jscad.subtract>
   {lidMagnets.map(([x,y])=><jscad.cylinder key={`m${x},${y}`} radius={2.4} height={2.1} center={[x,y,8.35]}/>)}
   {mountingHoles.map(([x,y])=><jscad.cylinder key={`p${x},${y}`} radius={2.4} height={5.8} center={[x,y,-3.7]}/>)}
   {/* Low locating tray plus foam adhesive: no hard clamp on the pouch. */}
   <jscad.subtract>
    <jscad.cuboid size={[34,41,2.6]} center={[0,-29,-5.2]}/>
    <jscad.cuboid size={[31,38,4]} center={[0,-29,-4.7]}/>
    <jscad.cuboid size={[12,8,4]} center={[-13,-9,-4.7]}/>
   </jscad.subtract>
   {/* Keyed web reconnects the upper locating tongue above the LCD board. */}
   <jscad.cuboid size={[10,3,0.5]} center={[0,46.4,8.25]}/>
   <jscad.cuboid size={[10,0.8,1.2]} center={[0,46.8,7.9]}/>
  </jscad.union>
  {mountingHoles.map(([x,y])=><jscad.cylinder key={`p${x},${y}`} radius={1} height={4.1} center={[x,y,-2.75]}/>)}
  {lidMagnets.map(([x,y])=><jscad.cylinder key={`m${x},${y}`} radius={1.6} height={1.2} center={[x,y,8.9]}/>)}
  {oledMounts.map(([x,y])=><jscad.cylinder key={`lcd-clear${x},${y}`} radius={2} height={4} center={[x,y,11]}/>)}
  <PortCuts/>
  <jscad.cuboid size={[53,44,3.4]} center={[0,17.2,8.9]}/>
  <jscad.cuboid size={[10,5,1]} center={[0,-61,7.4]}/>
 </jscad.subtract></jscad.colorize></jscad.translate>
 <jscad.translate offset={[0,0,-0.8]}><jscad.rectangle name="pcb" reference size={[60,90]}/></jscad.translate>
</Fragment>}
export function CaseLid({lift=0}: {lift?:number}){
 const w=caseDimensions.displayWindow
 return <jscad.translate offset={[0,0,lift]}><jscad.colorize color="#f5f6f4"><jscad.union>
  <jscad.subtract>
   <jscad.union>
    <jscad.subtract>
     <ShellSolid width={62.8} height={108.6} depth={3.2} z={7.4} radius={4}/>
     <ShellSolid width={61} height={106} depth={3} z={6.4} radius={2}/>
    </jscad.subtract>
    {oledMounts.map(([x,y])=><jscad.cylinder key={`lcd${x},${y}`} radius={1.7} height={1.3} center={[x,y,8.75]}/>)}
   </jscad.union>
   {oledMounts.map(([x,y])=><jscad.cylinder key={`lcd${x},${y}`} radius={0.75} height={2.5} center={[x,y,8.5]}/>)}
   {lidMagnets.map(([x,y])=><jscad.cylinder key={`m${x},${y}`} radius={1.6} height={1.2} center={[x,y,9.9]}/>)}
   <jscad.translate offset={[w.x,w.y,0]}><ShellSolid width={w.width} height={w.height} depth={7} z={6.9} radius={1.5} centerY={0}/></jscad.translate>
   <jscad.cuboid size={[51,26.6,2]} center={[0,17.2,9]}/>
   <jscad.cuboid size={[10.6,3.6,1.7]} center={[0,46.4,8.1]}/>
   <jscad.cylinder radius={27.35} height={8} center={[0,-27,8.8]}/>
  </jscad.subtract>
  {/* Diagonal ribs support the fixed wheel face between moving quadrants. */}
  {[45,135,225,315].map(a=><jscad.translate key={a} offset={[0,-27,0]}><jscad.rotate angles={[0,0,a*Math.PI/180]}>
   <jscad.cuboid size={[13,2.4,0.8]} center={[24,0,10.1]}/>
  </jscad.rotate></jscad.translate>)}
  <jscad.subtract>
   <jscad.cylinder radius={20.8} height={1.1} center={[0,-27,10.55]}/>
   <jscad.cylinder radius={7.8} height={4} center={[0,-27,10.05]}/>
  </jscad.subtract>
 </jscad.union></jscad.colorize></jscad.translate>
}
export function RingButton({angle=0,lift=0}: {angle?:number;lift?:number}){
 const points: [number,number][]=[]
 for(let i=0;i<=32;i++){const a=(-44.2+88.4*i/32)*Math.PI/180;points.push([27*Math.cos(a),27*Math.sin(a)])}
 for(let i=32;i>=0;i--){const a=(-44.2+88.4*i/32)*Math.PI/180;points.push([21.1*Math.cos(a),21.1*Math.sin(a)])}
 return <jscad.translate offset={[0,-27,lift]}><jscad.rotate angles={[0,0,angle*Math.PI/180]}><jscad.colorize color="#edf0f0"><jscad.subtract><jscad.union>
  <jscad.translate offset={[0,0,9.4]}><jscad.extrudeLinear height={2.1}><jscad.polygon points={points}/></jscad.extrudeLinear></jscad.translate>
  <jscad.cuboid size={[17.8,3,0.9]} center={[18.2,0,9.2]}/>
  <jscad.cylinder radius={1.3} height={5.79} center={[12,0,6.455]}/>
 </jscad.union>
  {[-45,45].map(a=><jscad.rotate key={a} angles={[0,0,a*Math.PI/180]}><jscad.cuboid size={[14,3,2.2]} center={[24,0,10.5]}/></jscad.rotate>)}
 </jscad.subtract></jscad.colorize></jscad.rotate></jscad.translate>
}
export function ButtonPlunger({lift=0}: {lift?:number}){return <jscad.translate offset={[0,-27,lift]}><jscad.colorize color="#edf0f0"><jscad.union>
 <jscad.cylinder radius={1.3} height={4.64} center={[0,0,5.88]}/>
 <jscad.cylinder radius={9.1} height={0.7} center={[0,0,8.15]}/>
 <jscad.cylinder radius={7.5} height={3.5} center={[0,0,9.75]}/>
</jscad.union></jscad.colorize></jscad.translate>}
export function EnclosureParts({lidLift=0}: {lidLift?:number}){return <Fragment>
 <assembly.printedpart name="CASE_BASE" displayName="Silver back + lower battery tray" jscad={<CaseBase drop={lidLift?-28:0}/>}/>
 <assembly.printedpart name="CASE_LID" displayName="White front + fixed wheel face" jscad={<CaseLid lift={lidLift}/>}/>
 <assembly.printedpart name="BUTTON_SELECT" displayName="Center select button" jscad={<ButtonPlunger lift={lidLift}/>}/>
 {Object.entries(ringAngles).map(([name,angle])=><assembly.printedpart key={name} name={`BUTTON_${name}`} displayName={`${name} quadrant button`} jscad={<RingButton angle={angle} lift={lidLift}/>}/>)}
 <assembly.screen name="WAVESHARE_19_LCD" connectsTo=".J_LCD" modelUrl={lidLift?oledExplodedModelUrl:oledModelUrl}/>
 <assembly.subassembly name="LCD_GH_CABLE_PLUG" cadModel={{jscad:models.displayPlug}}/>
 <assembly.subassembly name="CONTROL_LEGENDS" cadModel={{jscad:models.controlLegends,positionOffset:{x:0,y:0,z:lidLift}}}/>
 <assembly.subassembly name="BATTERY_1S_500MAH" displayName="Protected 3.7 V / 500 mAh rechargeable battery">
  {Object.entries(models.battery).map(([name,jscadPlan])=><assembly.subassembly key={name} name={`BATTERY_${name}`} cadModel={{jscad:jscadPlan,positionOffset:{x:lidLift?65:0,y:0,z:lidLift?-14:0}}}/>)}
 </assembly.subassembly>
</Fragment>}