Files
esphome-samsung-vfd/case/vfd-case.scad
T

301 lines
14 KiB
OpenSCAD
Raw Normal View History

// 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)
2026-11-01 13:03:53 +01:00
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
2026-11-01 13:03:53 +01:00
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;
/* [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 ----------
2026-11-01 13:03:53 +01:00
// 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;
2026-11-01 13:03:53 +01:00
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];
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() {
2026-11-01 13:03:53 +01:00
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("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 == "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(); }
// 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(); }
}