Files
esphome-samsung-vfd/case/vfd-case.scad
T
mangavalkandClaude Opus 5.5 592837c6de Case: two more body screws near the front
The base only screwed to the body at the back. New bosses in the side walls sit just behind the display, clear of the bezel's slide-in path; the base rim gets a gap around them and the feet part now has 6 feet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 21:24:44 +01:00

352 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 + 2 front side bosses in the body, 4 board standoffs on the base.
// 2. Bolt the display to the bezel from behind (4x M3x6).
// 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 (6x 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.2; // hole for the insert (CNC Kitchen M3x5.7; 4.0 printed too tight)
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 (M3x8 still fits)] */
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
boss_h = 10; // body boss height before the chamfer
rear_screws = [[rear_boss / 2, depth - wall - rear_boss / 2],
[inner_w - rear_boss / 2, depth - wall - rear_boss / 2]];
// front side bosses: far enough back that the display's back stays clear while
// the bezel slides in along the panel
n_disp_back = n_pcb + pcb_t + disp_back;
front_boss_y = ceil((n_disp_back + 2 + (boss_h + rear_boss) * sin(tilt)) / cos(tilt));
front_screws = [[rear_boss / 2, front_boss_y + rear_boss / 2],
[inner_w - rear_boss / 2, front_boss_y + rear_boss / 2]];
body_screws = concat(front_screws, rear_screws);
base_screws = concat([for (x = foot_xs) [x, foot_y]], body_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);
}
// bosses for the base screws, chamfered up into the walls
for (p = rear_screws) body_boss(p, true);
for (p = front_screws) body_boss(p, false);
}
// rear: in the corner, chamfered into the side and back walls; front: side wall only
module body_boss(p, rear) {
x0 = p[0] < inner_w / 2 ? 0 : inner_w - rear_boss;
xw = p[0] < inner_w / 2 ? 0 : inner_w - 0.01;
y0 = p[1] - rear_boss / 2;
difference() {
hull() {
translate([x0, y0, 0]) cube([rear_boss, rear_boss, boss_h]);
translate([xw, y0, 0]) cube([0.01, rear_boss, boss_h + rear_boss]);
if (rear) translate([x0, y0 + rear_boss - 0.01, 0]) cube([rear_boss, 0.01, boss_h + 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;
difference() {
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]);
for (p = front_screws) translate([p[0], p[1], 1])
cube([rear_boss + 2 * clr, rear_boss + 2 * clr, 4], center = true);
}
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]) // 6 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(); }
}