// ============================================================================ // Kidsbox — parametric 3D-printable enclosure // --------------------------------------------------------------------------- // Open in OpenSCAD and set `part` (top-right) or export from the CLI: // // openscad -D 'part="body"' -o stl/body.stl kidsbox.scad // openscad -D 'part="lid"' -o stl/lid.stl kidsbox.scad // openscad -D 'part="speaker_adapter"' -o stl/speaker_adapter.stl kidsbox.scad // openscad -D 'part="grill"' -o stl/grill.stl kidsbox.scad // // `part="assembly"` shows everything together (for preview only). // All tunables live in params.scad. // ============================================================================ include use part = "assembly"; // body | lid | speaker_adapter | grill | assembly // --------------------------------------------------------------------------- // Front-panel speaker interface (the FIXED "mount for the speaker mount") // --------------------------------------------------------------------------- // Subtractive: the sound opening + an outer recess for the grill flange. module speaker_cut(cx, cz) { translate([cx, 0, cz]) rotate([-90, 0, 0]) { // sound opening straight through the front wall translate([0, 0, -1]) cylinder(h = wall + 2, d = spk_sound_d); // grill flange recess on the OUTSIDE face translate([0, 0, -0.01]) cylinder(h = grill_t + 0.01, d = spk_sound_d + 8); } } // Additive: 4 bosses on the INSIDE so a swappable adapter can bolt on. module speaker_bosses(cx, cz) { translate([cx, 0, cz]) rotate([-90, 0, 0]) translate([0, 0, wall - 0.5]) bolt_circle(spk_bolt_d, spk_bolt_count, 45) boss(spk_boss_d, spk_boss_h + 0.5, 2.6); // self-tap for M3 } // --------------------------------------------------------------------------- // Button holes through the lid (row near the front edge) // --------------------------------------------------------------------------- module button_cuts() { for (i = [0 : btn_count - 1]) { x = box_w/2 + (i - (btn_count - 1) / 2) * btn_pitch; translate([x, btn_row_y, box_h - 1]) cylinder(h = lid_t + 2, d = btn_d); } } // --------------------------------------------------------------------------- // Side ports: USB/charge (right wall) and power switch (left wall) // --------------------------------------------------------------------------- module side_ports() { // USB on +X wall translate([box_w - wall - 1, box_d * 0.30, usb_center_z]) cube([wall + 2, usb_w, usb_h], center = false); // power switch on -X wall translate([-1, box_d * 0.30, sw_center_z]) rotate([0, 90, 0]) cylinder(h = wall + 2, d = sw_d); } // --------------------------------------------------------------------------- // Ventilation slots on the back wall (+Y) // --------------------------------------------------------------------------- module vents() { total = (vent_count - 1) * (vent_slot_w + 4); for (i = [0 : vent_count - 1]) { x = box_w/2 - total/2 + i * (vent_slot_w + 4); translate([x, box_d - wall - 1, box_h * 0.55]) cube([vent_slot_w, wall + 2, vent_slot_len]); } } // --------------------------------------------------------------------------- // Internal mounts // --------------------------------------------------------------------------- // Corner posts the lid screws into (fused into the wall corners, top-anchored). module lid_bosses() { boss_len = lid_rim_h + 10; for (sx = [lid_boss_inset, box_w - lid_boss_inset], sy = [lid_boss_inset, box_d - lid_boss_inset]) translate([sx, sy, box_h - boss_len]) boss(lid_boss_d, boss_len, lid_screw_d - 0.6); // self-tap } // ESP32-LyraT standoffs (board lies flat on the floor, toward the back). lyrat_cx = box_w/2; lyrat_cy = box_d - lyrat_d/2 - 10; module lyrat_standoffs() { for (sx = [-lyrat_hole_dx/2, lyrat_hole_dx/2], sy = [-lyrat_hole_dy/2, lyrat_hole_dy/2]) translate([lyrat_cx + sx, lyrat_cy + sy, floor_t]) boss(lyrat_standoff_d, lyrat_standoff_h, lyrat_screw_d); } // 2x 18650 battery cradle (open-top tray with end stops + clips), front-left. batt_cx = box_w/2; batt_cy = wall + batt_holder_d/2 + 4; module battery_cradle() { ow = batt_holder_w + 2*batt_wall; od = batt_holder_d + 2*batt_wall; translate([batt_cx - ow/2, batt_cy - od/2, floor_t]) difference() { cube([ow, od, batt_holder_h]); // hollow pocket, open top translate([batt_wall, batt_wall, batt_wall]) cube([batt_holder_w, batt_holder_d, batt_holder_h]); // cable notch in the front wall of the cradle translate([ow/2 - 6, -1, batt_holder_h - 8]) cube([12, batt_wall + 2, 10]); } if (batt_clip) { // two retaining lips along the long sides for (sy = [batt_cy - od/2 + batt_wall/2, batt_cy + od/2 - batt_wall/2]) translate([batt_cx - batt_holder_w/3, sy, floor_t + batt_holder_h]) rotate([0, 0, 0]) cube([batt_holder_w*2/3, batt_wall, 2.5], center = false); } } // --------------------------------------------------------------------------- // BODY // --------------------------------------------------------------------------- module body() { spk_l = box_w/2 - spk_spacing/2; spk_r = box_w/2 + spk_spacing/2; difference() { union() { shell([box_w, box_d, box_h], corner_r, wall, floor_t); speaker_bosses(spk_l, spk_center_z); speaker_bosses(spk_r, spk_center_z); lid_bosses(); lyrat_standoffs(); battery_cradle(); } speaker_cut(spk_l, spk_center_z); speaker_cut(spk_r, spk_center_z); side_ports(); vents(); } } // --------------------------------------------------------------------------- // LID (NFC reader sits underneath; print thin-side up) // --------------------------------------------------------------------------- // RC522 pocket posts on the lid underside. module rc522_mount() { // board centred, posts hang DOWN from the lid underside (local -Z here) bx = 54; by = 34; // RC522 mounting-hole rectangle (verify) for (sx = [-bx/2, bx/2], sy = [-by/2, by/2]) translate([sx, sy, -8]) cylinder(h = 8, d = 6); // pilot holes for (sx = [-bx/2, bx/2], sy = [-by/2, by/2]) translate([sx, sy, -8.5]) cylinder(h = 9, d = 2.4); } module lid() { difference() { union() { // top plate sitting on the wall tops translate([box_w/2, box_d/2, box_h]) linear_extrude(lid_t) rrect(box_w, box_d, corner_r); // locating lip dropping into the interior translate([box_w/2, box_d/2, box_h - lid_rim_h]) linear_extrude(lid_rim_h) difference() { rrect(inner_w - 2*fit, inner_d - 2*fit, max(0.1, corner_r - wall)); rrect(inner_w - 2*fit - 2*lid_rim_t, inner_d - 2*fit - 2*lid_rim_t, max(0.1, corner_r - wall - lid_rim_t)); } // RC522 mount, centred under the lid translate([box_w/2, box_d/2, box_h + lid_t]) rc522_mount(); } // corner screw holes (countersunk) through the plate for (sx = [lid_boss_inset, box_w - lid_boss_inset], sy = [lid_boss_inset, box_d - lid_boss_inset]) { translate([sx, sy, box_h - 0.5]) cylinder(h = lid_t + 1, d = lid_screw_d); translate([sx, sy, box_h + lid_t - 1.4]) cylinder(h = 1.6, d1 = lid_screw_d, d2 = lid_screw_d + 3.5); } // button holes near the front edge button_cuts(); } } // --------------------------------------------------------------------------- // SWAPPABLE SPEAKER ADAPTER (print one per speaker model) // --------------------------------------------------------------------------- module speaker_adapter() { difference() { union() { cylinder(h = adapter_t, d = adapter_outer_d); // frame seat ring raised on the back so the speaker locates translate([0, 0, adapter_t]) ring(spk_frame_d + 6, spk_cutout_d, spk_frame_seat); } // cone clearance translate([0, 0, -0.5]) cylinder(h = adapter_t + spk_frame_seat + 1, d = spk_cutout_d); // speaker frame recess on the back translate([0, 0, adapter_t - 0.01]) cylinder(h = spk_frame_seat + 0.02, d = spk_frame_d); // mounting holes to the panel bosses (FIXED interface) bolt_circle(spk_bolt_d, spk_bolt_count, 45) translate([0, 0, -0.5]) cylinder(h = adapter_t + spk_frame_seat + 1, d = spk_bolt_hole_d); // the speaker's OWN screw holes (swappable per model) bolt_circle(spk_screw_bolt_d, spk_screw_count, 0) translate([0, 0, -0.5]) cylinder(h = adapter_t + spk_frame_seat + 1, d = spk_screw_d); } } // --------------------------------------------------------------------------- // SPEAKER GRILL (snaps into the sound opening) // --------------------------------------------------------------------------- module grill() { r_out = spk_sound_d/2; union() { // outer flange that seats in the panel recess ring(spk_sound_d + 8, spk_sound_d - 4, grill_t); // friction lip into the hole translate([0, 0, -grill_lip_h]) ring(spk_sound_d - 2*fit, spk_sound_d - 2*fit - 3, grill_lip_h + grill_t); // concentric rings for (i = [1 : grill_rings]) ring(2*r_out * i/(grill_rings+1) + 1.4, 2*r_out * i/(grill_rings+1) - 1.4, grill_t); // central hub cylinder(h = grill_t, d = 6); // radial spokes for (i = [0 : grill_spokes - 1]) rotate([0, 0, i * 360/grill_spokes]) translate([0, -1.0, 0]) cube([r_out, 2.0, grill_t]); } } // --------------------------------------------------------------------------- // ASSEMBLY PREVIEW // --------------------------------------------------------------------------- explode = 45; // lid lift for the exploded preview (0 = closed) module assembly() { spk_l = box_w/2 - spk_spacing/2; spk_r = box_w/2 + spk_spacing/2; color("WhiteSmoke") body(); color("Gainsboro") translate([0, 0, explode]) lid(); for (cx = [spk_l, spk_r]) { // adapter just inside the front wall color("SteelBlue") translate([cx, wall + adapter_t, spk_center_z]) rotate([90, 0, 0]) speaker_adapter(); // grill in the opening color("DimGray") translate([cx, grill_t, spk_center_z]) rotate([-90, 0, 0]) grill(); } } // --------------------------------------------------------------------------- // DISPATCH // --------------------------------------------------------------------------- if (part == "body") body(); else if (part == "lid") lid(); else if (part == "speaker_adapter") speaker_adapter(); else if (part == "grill") grill(); else assembly();