from cadgen import build123d as bd # Millimetres. These are design assumptions until the received boards are measured. OUT_X = 76.0 OUT_Y = 54.0 BASE_H = 18.0 LID_Z = 17.0 LID_H = 13.0 WALL = 2.4 FIT_CLEARANCE = 0.6 CAM_OPEN_X = 14.0 CAM_OPEN_Z = 6.0 SPEAKER_CENTER_X = 12.0 SPEAKER_CENTER_Y = 0.0 SPEAKER_OPEN_RADIUS = 17.0 MIC_PORT_X = -20.0 MIC_PORT_Z = 10.0 MIC_PORT_RADIUS = 1.0 def _box(x, y, z, loc=(0, 0, 0), align=None): if align is None: align = (bd.Align.CENTER, bd.Align.CENTER, bd.Align.MIN) return bd.Box(x, y, z, align=align).moved(bd.Location(loc)) def _cylinder(radius, height, loc=(0, 0, 0)): return bd.Cylinder(radius, height, align=(bd.Align.CENTER, bd.Align.CENTER, bd.Align.MIN)).moved(bd.Location(loc)) def base_shell(): # Build the tray from a bottom plate and four closed wall solids. This # leaves a real open cavity while keeping the STL as a watertight solid. bottom = _box(OUT_X, OUT_Y, WALL) front = _box(OUT_X, WALL, BASE_H, loc=(0, OUT_Y / 2 - WALL / 2, 0)) back = _box(OUT_X, WALL, BASE_H, loc=(0, -OUT_Y / 2 + WALL / 2, 0)) left = _box(WALL, OUT_Y - 2 * WALL, BASE_H, loc=(-OUT_X / 2 + WALL / 2, 0, 0)) right = _box(WALL, OUT_Y - 2 * WALL, BASE_H, loc=(OUT_X / 2 - WALL / 2, 0, 0)) body = bottom + front + back + left + right # Small acoustic port for the INMP441. The microphone location is kept # adjustable because the breakout's seller-specific placement is not # published; keep the port away from the speaker opening and use foam as # an acoustic baffle if needed. mic_port = bd.Cylinder(MIC_PORT_RADIUS, 4.0, align=(bd.Align.CENTER, bd.Align.CENTER, bd.Align.MIN)) mic_port = mic_port.rotate(bd.Axis.X, 90).moved( bd.Location((MIC_PORT_X, OUT_Y / 2 + 2.0, MIC_PORT_Z))) body = body - mic_port # A speaker this size cannot fit behind an 18 mm-high front grille. It is # therefore mounted face-up under the lid, where the 54 x 76 mm top face # provides a top-facing acoustic opening. # Battery, microphone and amplifier use removable foam/strap retention in # the open cavity. Their seller-specific hole patterns are not published, # so hard-coded bosses here would reduce fit tolerance rather than improve it. body.label = "base_shell" return body def lid_shell(): outer = _box(OUT_X, OUT_Y, LID_H, loc=(0, 0, LID_Z)) # Lid is an open-bottom cap with a 2.4 mm top. inner_h = LID_H - WALL + 0.2 inner = _box(OUT_X - 2 * WALL + FIT_CLEARANCE, OUT_Y - 2 * WALL + FIT_CLEARANCE, inner_h, loc=(0, 0, LID_Z - 0.2)) body = outer - inner # Front camera window. The actual lens board is adjustable by CAM_OPEN_X/CAM_OPEN_Z. camera_window = _box(CAM_OPEN_X, 4.0, CAM_OPEN_Z, loc=(0, OUT_Y / 2 - 1.8, LID_Z + 5.0), align=(bd.Align.CENTER, bd.Align.CENTER, bd.Align.CENTER)) body = body - camera_window # Ø34 mm acoustic opening through the lid top. The 40 mm speaker frame # overlaps this opening by 3 mm on every side and is retained with foam, # adhesive, or a strap after the received speaker thickness is measured. speaker_hole = bd.Cylinder(SPEAKER_OPEN_RADIUS, 4.0, align=(bd.Align.CENTER, bd.Align.CENTER, bd.Align.MIN)) # Start the cutter below the inner ceiling. Starting it at the outer top # would leave a 0.4 mm membrane (30 - 27.6 - 2.0) under the opening. speaker_hole = speaker_hole.moved(bd.Location((SPEAKER_CENTER_X, SPEAKER_CENTER_Y, LID_Z + LID_H - WALL - 0.2))) body = body - speaker_hole # Left-side USB-C service cutout; leaves the board connector accessible. # The cut is deliberately kept away from the top and bottom edges to avoid # a thin sliver that is difficult to print reliably. usb_cutout = _box(4.0, 12.0, 5.0, loc=(-OUT_X / 2 + 1.8, 0.0, LID_Z + 5.0), align=(bd.Align.CENTER, bd.Align.CENTER, bd.Align.CENTER)) body = body - usb_cutout # There are intentionally no screw holes or printed posts in this revision: # the seller-specific board positions are not published. Close the lid by # friction fit plus foam/adhesive or a soft strap; add fastener datums only # after the received parts have been measured. body.label = "lid_shell" return body def enclosure_assembly(): base = base_shell() lid = lid_shell() base.label = "base_shell" lid.label = "lid_shell" return bd.Compound(children=[base, lid], label="esp32_voice_cam_enclosure")