mirror of
https://github.com/sweidac/kidsbox.git
synced 2026-10-27 22:52:57 +01:00
A printable reimagining of the wooden Kidsbox: rectangular cabinet with two front speakers, removable top lid (NFC tag rests there, reader mounts beneath), and swappable speaker mounts. - kidsbox.scad: master model + part dispatcher (body/lid/speaker_adapter/grill) - params.scad: all tunable dimensions in one place - lib.scad: helper modules (rounded shell, bosses, bolt circles) - Fixed front-panel speaker interface + swappable adapter so any driver fits by printing a new adapter (edit spk_* params, re-export) - Internal mounts: 2x18650 cradle, ESP32-LyraT standoffs, RC522 under lid, 3 buttons on lid, USB/charge + power-switch side ports, back-wall vents - Snap-in speaker grills - Makefile to export STLs and render previews (headless via xvfb) - Exported STLs and preview renders included - Link enclosure from main README Dimensions are sensible defaults from the reference photos and common parts; README flags the fit-critical values to verify before mass-printing. https://claude.ai/code/session_01MhAnh68DPNnTjcKqjNYDmk
276 lines
11 KiB
OpenSCAD
276 lines
11 KiB
OpenSCAD
// ============================================================================
|
|
// 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 <params.scad>
|
|
use <lib.scad>
|
|
|
|
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();
|