Each foot has a spigot that locates in the base's screw counterbore and a pocket for the screw head; base screws become M3x10. Base unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
336 lines
16 KiB
OpenSCAD
336 lines
16 KiB
OpenSCAD
// Desk case for the Samsung 20L203 2x20 VFD and a WEMOS D1 R1 (UNO-sized ESP8266).
|
|
//
|
|
// Three parts, all printable without supports:
|
|
// body sides, top and back wall (DC jack + USB cut-outs). Print upside down, top on the bed.
|
|
// bezel 15 degree front panel with the window and the display bosses. Print face down.
|
|
// base floor with the board standoffs. Print flat.
|
|
//
|
|
// Assembly:
|
|
// 1. Melt M3 heat-set inserts into: 4 display bosses + 2 feet on the bezel,
|
|
// 2 rear corner bosses in the body, 4 board standoffs on the base.
|
|
// 2. Bolt the display to the bezel from behind (4x M3x8).
|
|
// 3. Screw the D1 board onto the base (4x M3x6).
|
|
// 4. Slide the bezel (with display) up into the body's side grooves from below.
|
|
// 5. Wire up, put the body on the base and screw it together from below (4x M3x8).
|
|
//
|
|
// Render one part: openscad -D 'part="body"' -o body.stl vfd-case.scad
|
|
// Parts: "assembly" (preview with dummy display + board), "body", "bezel", "base",
|
|
// "check_*" (collision checks, see the end of this file).
|
|
|
|
part = "assembly";
|
|
section_x = 130; // cut position for part = "section"
|
|
|
|
/* [Case] */
|
|
tilt = 15; // display tilt back from upright, degrees
|
|
wall = 3; // side, top and back wall thickness
|
|
lip = 1.5; // how far the bezel sits behind the front edge of the body
|
|
bezel_t = 3; // bezel thickness
|
|
groove = 1.5; // how deep the bezel edges sit in the side walls
|
|
clr = 0.3; // fit clearance for sliding / mating parts
|
|
base_t = 3.5; // base plate thickness
|
|
depth = 112; // outer depth (front bottom edge to back)
|
|
top_margin = 4; // bezel above the display's top edge (along the panel)
|
|
|
|
/* [Display module, datasheet 4.10] */
|
|
disp_w = 190;
|
|
disp_h = 64;
|
|
disp_hole_inset = 4; // hole centres from the PCB edges (182 x 56 spacing)
|
|
glass_x0 = 3.0; // glass left edge from PCB left edge (datasheet front view)
|
|
glass_y0 = 7.85; // glass bottom edge from PCB bottom edge (datasheet front view)
|
|
glass_w = 175.2;
|
|
glass_h = 48.3;
|
|
glass_depth = 12.0; // glass front face to PCB front face
|
|
glass_gap = 0.5; // air gap between bezel and glass (datasheet asks for one)
|
|
pcb_t = 1.6;
|
|
disp_back = 11; // components behind the display PCB
|
|
disp_raise = 14; // PCB bottom edge, measured along the panel from the bottom
|
|
disp_rotated = true; // display turned 180 deg from the datasheet's front view
|
|
// (jumper header on the left seen from the front); the
|
|
// glass is off-centre, so this moves the bosses 8.8 mm
|
|
|
|
/* [Window] */
|
|
win_w = 156;
|
|
win_h = 36;
|
|
win_r = 2;
|
|
win_chamfer = 1.5;
|
|
|
|
/* [Heat-set inserts, M3] */
|
|
insert_d = 4.0; // hole for the insert
|
|
insert_depth = 6;
|
|
boss_d = 7; // display bosses (larger would touch the glass)
|
|
screw_d = 3.4; // M3 clearance
|
|
head_d = 6.2; // M3 head counterbore in the base
|
|
head_depth = 2;
|
|
|
|
/* [TPU feet, held by the base screws (use M3x10 instead of M3x8)] */
|
|
foot_d = 14; // fits inside the base outline at the rear corners
|
|
foot_h = 4.5; // below the base
|
|
foot_spigot_h = 1.8; // locates in the base's screw counterbore
|
|
foot_spigot_d = head_d - 0.2;
|
|
foot_head_d = 6.6; // pocket for the screw head (from below)
|
|
foot_head_depth = 3.5; // M3 socket head is 3 mm tall
|
|
|
|
/* [WEMOS D1 R1 (Arduino UNO footprint)] */
|
|
board_l = 68.58; // along the case depth, connector edge at the back
|
|
board_w = 53.34;
|
|
board_gap = 0.5; // board edge to back wall
|
|
standoff_h = 6;
|
|
standoff_d = 7;
|
|
// UNO hole positions: (distance from the connector edge, distance from the jack side)
|
|
board_holes = [[13.97, 2.54], [15.24, 50.8], [66.04, 7.62], [66.04, 35.56]];
|
|
jack_pos = 7.6; // DC jack centre from the board's jack-side edge
|
|
jack_cut = [12, 13]; // width, height (from 0.5 below the PCB top)
|
|
usb_pos = 38.1; // micro-USB centre from the jack-side edge -- measure yours!
|
|
usb_cut = [12, 8]; // width, height (centred 1.6 above the PCB top)
|
|
board_comp_h = 10; // tallest part on the board (dummy for the collision check)
|
|
|
|
$fn = 48;
|
|
|
|
// ---------- derived ----------
|
|
// glass position from the PCB's bottom-left corner as mounted (front view)
|
|
gl_x = disp_rotated ? disp_w - glass_x0 - glass_w : glass_x0;
|
|
gl_y = disp_rotated ? disp_h - glass_y0 - glass_h : glass_y0;
|
|
gx_c = gl_x + glass_w / 2; // glass centre from PCB left
|
|
inner_w = 2 * (max(gx_c, disp_w - gx_c) + 1.5); // window centred, 1.5 mm side clearance
|
|
outer_w = inner_w + 2 * wall;
|
|
disp_x0 = inner_w / 2 - gx_c; // PCB left edge, x
|
|
z_top_in = (disp_raise + disp_h + top_margin) * cos(tilt); // top wall underside
|
|
height = z_top_in + wall;
|
|
n_bezel_in = lip + bezel_t; // bezel inner face (local N)
|
|
n_glass = n_bezel_in + glass_gap;
|
|
n_pcb = n_glass + glass_depth;
|
|
win_s = disp_raise + gl_y + glass_h / 2; // window centre along the panel
|
|
board_x0 = inner_w / 2 - board_w / 2;
|
|
board_conn_y = depth - wall - board_gap; // y of the board's connector edge
|
|
pcb_top_z = standoff_h + pcb_t;
|
|
foot_n = n_bezel_in + 6; // insert axis in the bezel feet
|
|
foot_y = foot_n / cos(tilt);
|
|
foot_xs = [inner_w * 0.25, inner_w * 0.75];
|
|
rear_boss = 9; // rear corner boss block size
|
|
rear_screws = [[rear_boss / 2, depth - wall - rear_boss / 2],
|
|
[inner_w - rear_boss / 2, depth - wall - rear_boss / 2]];
|
|
base_screws = concat([for (x = foot_xs) [x, foot_y]], rear_screws);
|
|
|
|
echo(str("Outer size: ", outer_w, " x ", depth, " x ", height + base_t, " mm (W x D x H)"));
|
|
|
|
// Panel-local frame: X = across, S = up the panel from the bottom front edge,
|
|
// N = into the case from the body's front plane. The matrix is its own inverse.
|
|
M = [[1, 0, 0, 0],
|
|
[0, sin(tilt), cos(tilt), 0],
|
|
[0, cos(tilt), -sin(tilt), 0],
|
|
[0, 0, 0, 1]];
|
|
module panel() multmatrix(M) children();
|
|
|
|
module rrect(w, h, r) {
|
|
offset(r) square([w - 2 * r, h - 2 * r], center = true);
|
|
}
|
|
|
|
// 2D side profile (y, z) extruded along x.
|
|
module side_extrude(w) rotate([90, 0, 90]) linear_extrude(w) children();
|
|
|
|
// ---------- body ----------
|
|
module body() {
|
|
difference() {
|
|
union() {
|
|
translate([-wall, 0, 0]) side_extrude(outer_w)
|
|
polygon([[0, 0], [depth, 0], [depth, height], [height * tan(tilt), height]]);
|
|
}
|
|
// cavity, open at the front and bottom
|
|
translate([0, -50, -1]) cube([inner_w, depth - wall + 50, z_top_in + 1]);
|
|
// bezel grooves in the side walls
|
|
intersection() {
|
|
panel() translate([-groove - clr, -50, lip]) cube([inner_w + 2 * (groove + clr), 300, bezel_t + clr]);
|
|
translate([-50, -50, -1]) cube([inner_w + 100, depth + 100, z_top_in + 1]);
|
|
}
|
|
// DC jack and USB
|
|
for (c = [[jack_pos, jack_cut, pcb_top_z - 0.5 + jack_cut[1] / 2],
|
|
[usb_pos, usb_cut, pcb_top_z + 1.6]])
|
|
translate([board_x0 + c[0], depth - wall / 2, c[2]])
|
|
rotate([90, 0, 0]) linear_extrude(wall + 2, center = true) rrect(c[1][0], c[1][1], 1.5);
|
|
// top vents over the back half
|
|
for (i = [0 : 3]) translate([inner_w / 2, depth - wall - 14 - i * 8, height - wall / 2])
|
|
linear_extrude(wall + 2, center = true) rrect(inner_w - 50, 3, 1.4);
|
|
}
|
|
// rear corner bosses for the base screws, chamfered up into the walls
|
|
for (p = rear_screws) {
|
|
x0 = p[0] < inner_w / 2 ? 0 : inner_w - rear_boss;
|
|
xw = p[0] < inner_w / 2 ? 0 : inner_w - 0.01;
|
|
difference() {
|
|
hull() {
|
|
translate([x0, depth - wall - rear_boss, 0]) cube([rear_boss, rear_boss, 10]);
|
|
translate([xw, depth - wall - 0.01, 0]) cube([0.01, 0.01, 10 + rear_boss]);
|
|
translate([x0, depth - wall - 0.01, 0]) cube([rear_boss, 0.01, 10 + rear_boss]);
|
|
translate([xw, depth - wall - rear_boss, 0]) cube([0.01, rear_boss, 10 + rear_boss]);
|
|
}
|
|
translate([p[0], p[1], -1]) cylinder(d = insert_d, h = insert_depth + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------- bezel ----------
|
|
display_holes = [for (hx = [disp_hole_inset, disp_w - disp_hole_inset])
|
|
for (hy = [disp_hole_inset, disp_h - disp_hole_inset])
|
|
[disp_x0 + hx, disp_raise + hy]];
|
|
|
|
module bezel() {
|
|
difference() {
|
|
intersection() {
|
|
panel() union() {
|
|
// plate
|
|
translate([-groove + clr, -50, lip]) cube([inner_w + 2 * (groove - clr), 300, bezel_t]);
|
|
// display bosses, from the bezel to the PCB front face
|
|
for (h = display_holes) translate([h[0], h[1], n_bezel_in - 0.01])
|
|
cylinder(d = boss_d, h = n_pcb - n_bezel_in + 0.01);
|
|
// feet that take the front base screws
|
|
for (x = foot_xs) translate([x - 6, -10, n_bezel_in - 0.01]) cube([12, 22, 12]);
|
|
}
|
|
// flat bottom on the base, flat top against the top wall
|
|
translate([-50, -50, 0]) cube([inner_w + 100, depth + 100, z_top_in - clr]);
|
|
}
|
|
// window, chamfered on the front
|
|
panel() translate([inner_w / 2, win_s, 0]) {
|
|
translate([0, 0, lip - 1]) linear_extrude(bezel_t + 2) rrect(win_w, win_h, win_r);
|
|
hull() {
|
|
translate([0, 0, lip - 0.01]) linear_extrude(0.01) rrect(win_w + 2 * win_chamfer, win_h + 2 * win_chamfer, win_r + win_chamfer);
|
|
translate([0, 0, lip + win_chamfer]) linear_extrude(0.01) rrect(win_w, win_h, win_r);
|
|
}
|
|
}
|
|
// inserts in the display bosses
|
|
panel() for (h = display_holes) translate([h[0], h[1], n_pcb - insert_depth])
|
|
cylinder(d = insert_d, h = insert_depth + 1);
|
|
// inserts in the feet (vertical)
|
|
for (x = foot_xs) translate([x, foot_y, -1]) cylinder(d = insert_d, h = insert_depth + 1);
|
|
}
|
|
}
|
|
|
|
// ---------- base ----------
|
|
module base() {
|
|
difference() {
|
|
union() {
|
|
translate([-wall, 0, -base_t]) cube([outer_w, depth, base_t]);
|
|
// locating rim inside the side and back walls (stops short of the bezel and bosses)
|
|
rim_y0 = 20;
|
|
for (x = [clr, inner_w - clr - 1.6])
|
|
translate([x, rim_y0, -0.01]) cube([1.6, depth - wall - rear_boss - clr - rim_y0, 2]);
|
|
translate([rear_boss + clr, depth - wall - clr - 1.6, -0.01])
|
|
cube([inner_w - 2 * (rear_boss + clr), 1.6, 2]);
|
|
// board standoffs
|
|
for (h = board_holes) translate(board_xy(h)) cylinder(d = standoff_d, h = standoff_h);
|
|
// cable-tie anchor for the MAX3232 / display cable
|
|
translate([inner_w / 2, board_conn_y - board_l - 4.5, 0]) tie_anchor();
|
|
}
|
|
// screws into the body / bezel inserts, counterbored from below
|
|
for (p = base_screws) translate([p[0], p[1], -base_t - 1]) {
|
|
cylinder(d = screw_d, h = base_t + 2);
|
|
cylinder(d = head_d, h = head_depth + 1);
|
|
}
|
|
// board standoff inserts
|
|
for (h = board_holes) translate(board_xy(h)) translate([0, 0, standoff_h - insert_depth + 0.5])
|
|
cylinder(d = insert_d, h = insert_depth);
|
|
// vents under the display
|
|
for (i = [0 : 2]) translate([inner_w / 2, 20 + i * 7, -base_t / 2])
|
|
linear_extrude(base_t + 2, center = true) rrect(inner_w - 70, 3, 1.4);
|
|
}
|
|
}
|
|
|
|
function board_xy(h) = [board_x0 + h[1], board_conn_y - h[0], 0];
|
|
|
|
// One foot, top face at z = 0 (against the base's underside), centred on a screw.
|
|
module foot() {
|
|
difference() {
|
|
union() {
|
|
translate([0, 0, -foot_h]) cylinder(d = foot_d, h = foot_h);
|
|
translate([0, 0, -0.01]) cylinder(d = foot_spigot_d, h = foot_spigot_h + 0.01);
|
|
}
|
|
translate([0, 0, -foot_h - 1]) cylinder(d = screw_d, h = foot_h + foot_spigot_h + 2);
|
|
translate([0, 0, -foot_h - 1]) cylinder(d = foot_head_d, h = foot_head_depth + 1);
|
|
// soften the bottom edge
|
|
translate([0, 0, -foot_h - 0.01]) difference() {
|
|
cylinder(d = foot_d + 1, h = 1);
|
|
cylinder(d1 = foot_d - 2, d2 = foot_d + 0.01, h = 1.01);
|
|
}
|
|
}
|
|
}
|
|
|
|
module feet_in_place() {
|
|
for (p = base_screws) translate([p[0], p[1], -base_t]) foot();
|
|
}
|
|
|
|
module tie_anchor() {
|
|
difference() {
|
|
translate([-5, -3, 0]) cube([10, 6, 5]);
|
|
translate([-2.5, -4, -0.01]) cube([5, 8, 3.2]);
|
|
}
|
|
}
|
|
|
|
// ---------- dummies for the preview and the collision checks ----------
|
|
module display_dummy() {
|
|
panel() {
|
|
translate([disp_x0 + gl_x, disp_raise + gl_y, n_glass + 0.01])
|
|
cube([glass_w, glass_h, glass_depth - 0.02]);
|
|
translate([disp_x0, disp_raise, n_pcb + 0.01]) difference() {
|
|
cube([disp_w, disp_h, pcb_t + disp_back]);
|
|
for (hx = [disp_hole_inset, disp_w - disp_hole_inset])
|
|
for (hy = [disp_hole_inset, disp_h - disp_hole_inset])
|
|
translate([hx, hy, -1]) cylinder(d = 3.5, h = pcb_t + 2);
|
|
}
|
|
}
|
|
}
|
|
|
|
module board_dummy() {
|
|
translate([board_x0, board_conn_y - board_l, standoff_h + 0.01]) difference() {
|
|
union() {
|
|
cube([board_w, board_l, pcb_t]);
|
|
translate([2, 2, pcb_t]) cube([board_w - 4, board_l - 4, board_comp_h]);
|
|
// DC jack, sticking out 1 mm past the board edge
|
|
translate([jack_pos - 4.5, board_l - 14, pcb_t]) cube([9, 15, 11]);
|
|
}
|
|
for (h = board_holes) translate([h[1], board_l - h[0], -1]) cylinder(d = 3.2, h = pcb_t + 2);
|
|
}
|
|
}
|
|
|
|
// ---------- output ----------
|
|
if (part == "assembly") {
|
|
color("SlateGray", 0.5) body();
|
|
color("DimGray") bezel();
|
|
color("Gainsboro") base();
|
|
color("Black") feet_in_place();
|
|
color("SeaGreen") display_dummy();
|
|
color("SteelBlue") board_dummy();
|
|
} else if (part == "section") {
|
|
// assembly cut at x = section_x, for checking the inside
|
|
intersection() {
|
|
union() {
|
|
color("SlateGray") body();
|
|
color("DimGray") bezel();
|
|
color("Gainsboro") base();
|
|
color("SeaGreen") display_dummy();
|
|
color("SteelBlue") board_dummy();
|
|
}
|
|
translate([section_x, -50, -50]) cube([500, 500, 500]);
|
|
}
|
|
} else if (part == "body") {
|
|
translate([0, depth, height]) rotate([180, 0, 0]) body(); // top on the bed
|
|
} else if (part == "bezel") {
|
|
translate([0, 0, -lip]) multmatrix(M) bezel(); // front face on the bed
|
|
} else if (part == "feet") {
|
|
for (i = [0 : len(base_screws) - 1]) // 4 feet, flat side down
|
|
translate([i * (foot_d + 5), 0, foot_h]) foot();
|
|
} else if (part == "base") {
|
|
translate([0, 0, base_t]) base();
|
|
}
|
|
// Collision checks: each must render as an empty object.
|
|
else if (part == "check_body_bezel") intersection() { body(); bezel(); }
|
|
else if (part == "check_body_display") intersection() { body(); display_dummy(); }
|
|
else if (part == "check_bezel_display") intersection() { bezel(); display_dummy(); }
|
|
else if (part == "check_body_base") intersection() { body(); base(); }
|
|
else if (part == "check_bezel_base") intersection() { bezel(); base(); }
|
|
else if (part == "check_board") intersection() { union() { body(); bezel(); base(); display_dummy(); } board_dummy(); }
|
|
else if (part == "check_display_base") intersection() { base(); display_dummy(); }
|
|
else if (part == "check_feet_base") intersection() { base(); feet_in_place(); }
|
|
// The bezel slides in from below along the panel: sweep it (with the display) down and check.
|
|
else if (part == "check_insertion") intersection() {
|
|
body();
|
|
for (k = [5 : 5 : 80]) translate(-k * [0, sin(tilt), cos(tilt)]) { bezel(); display_dummy(); }
|
|
}
|