Add 3D-printable desk case for the VFD and a WEMOS D1 R1

Parametric OpenSCAD design: 15 degree bezel holding the display on M3
heat-set inserts, slides into the body; base carries the board with the
DC jack and USB reachable through the back. Includes STLs, previews and
collision checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-31 19:32:43 +01:00
1 parent 95686a3608
commit 0cf5df9b6e
9 files changed
+388

No files matched your search

+4
View File
@@ -20,6 +20,10 @@ server, so `type: git` with a URL, not `github://`). The layout must stay
- `docs/20L203DA12-datasheet.pdf`: the command set (section 5.4), jumpers, - `docs/20L203DA12-datasheet.pdf`: the command set (section 5.4), jumpers,
wiring and the CG-ROM tables (section 6). Check it before changing the wiring and the CG-ROM tables (section 6). Check it before changing the
protocol. protocol.
- `case/vfd-case.scad`: parametric OpenSCAD desk case (body, bezel, base) for
the display and a WEMOS D1 R1. After any change, run all the `check_*` parts
(each must be empty or zero-volume) and re-export `case/stl/*.stl`. On
Windows, call `openscad.com` for console output.
- `examples/samsung-vfd.yaml`: the full reference device config. It pulls the - `examples/samsung-vfd.yaml`: the full reference device config. It pulls the
component from git; for local work, switch to the commented component from git; for local work, switch to the commented
`type: local, path: ../components` source. `type: local, path: ../components` source.
+1
View File
@@ -22,6 +22,7 @@ components/samsung_vfd/ the component (Python schema + C++)
examples/samsung-vfd.yaml full example device config examples/samsung-vfd.yaml full example device config
examples/secrets.yaml.example examples/secrets.yaml.example
docs/ photos and the 20L203DA12 datasheet docs/ photos and the 20L203DA12 datasheet
case/ 3D-printable desk case (OpenSCAD + STL), see case/README.md
tools/gen_charmaps.py regenerates components/samsung_vfd/charmaps.h tools/gen_charmaps.py regenerates components/samsung_vfd/charmaps.h
``` ```
+88
View File
@@ -0,0 +1,88 @@
# Desk case
A 3D-printable desk case for the Samsung 20L203 VFD and a WEMOS D1 R1
(UNO-sized ESP8266 board). The display is tilted back 15° and bolted to the
front with M3 screws. The DC jack and the USB port are reachable through the
back wall.
<p>
<img src="preview.png" alt="Case preview" width="49%">
<img src="section.png" alt="Cross-section: display on the bezel, board on the base" width="49%">
</p>
Outer size: about 208 × 112 × 86 mm (W × D × H). The parts need a print bed of
at least 210 × 115 mm.
## Parts
| File | Print orientation | Notes |
|------|-------------------|-------|
| [stl/body.stl](stl/body.stl) | upside down, top on the bed (as exported) | Sides, top and back wall. No supports needed. |
| [stl/bezel.stl](stl/bezel.stl) | face down (as exported) | Front panel with the window and the display bosses. |
| [stl/base.stl](stl/base.stl) | flat (as exported) | Floor with the board standoffs and the screw holes. |
PETG is a good choice: the display warms the inside by a few degrees. PLA
works if the case stays out of the sun.
## Hardware
- 10 × M3 heat-set inserts (about 4.6 mm OD, 5.7 mm long; the holes are 4.0 mm
and 6 mm deep):
- 4 in the bezel's display bosses
- 2 in the bezel's feet
- 2 in the body's rear corners
- 4 on the base's board standoffs
- 4 × M3×8 to bolt the display to the bezel
- 4 × M3×6 to hold the D1 board on its standoffs
- 4 × M3×8 to hold the base on, from below
## Assembly
1. Melt the inserts in.
2. Bolt the display to the bezel from behind, glass facing the window.
3. Screw the D1 board onto the base, with the DC jack and USB edge at the back.
4. Slide the bezel, with the display on it, into the body from below. Its edges
run in the grooves in the side walls until it stops against the top.
5. Connect the display cable. The MAX3232 sits inline in the cable; tie it to
the anchor on the base.
6. Put the body on the base and screw it together from below. The base holds
the bezel in place.
## Measure before printing
The dimensions come from the 20L203**DA12** datasheet. Your board is a DA5, so
check these against yours and change the values at the top of
[vfd-case.scad](vfd-case.scad) if needed:
- `disp_hole_inset`, `disp_w`, `disp_h`: the display's mounting holes, 182 ×
56 mm apart.
- `glass_x0`, `glass_y0`, `glass_w`, `glass_h`, `glass_depth`: the glass
position and the distance from its front face to the PCB.
- `usb_pos`: where the micro-USB port is along the D1's back edge (38 mm from
the jack side is a guess based on the UNO layout). Also check `jack_pos`.
If only the display is off, it's enough to reprint the bezel.
## Changing the design
Open [vfd-case.scad](vfd-case.scad) in OpenSCAD. Every dimension is a variable
at the top, including the tilt, wall thickness and clearances. Export a part
with:
```
openscad -D 'part="body"' --export-format binstl -o stl/body.stl vfd-case.scad
openscad -D 'part="bezel"' --export-format binstl -o stl/bezel.stl vfd-case.scad
openscad -D 'part="base"' --export-format binstl -o stl/base.stl vfd-case.scad
```
`part="assembly"` shows everything with dummy display and board models, and
`part="section"` shows a cut-away. The `check_*` parts are collision checks;
each one must render as an empty object:
- `check_body_bezel`, `check_body_display`, `check_bezel_display`
- `check_body_base`, `check_bezel_base`, `check_display_base`
- `check_board`
- `check_insertion`: sweeps the bezel and display along their slide-in path
through the body.
Faces that only touch can show up as a zero-volume result, which is fine.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+295
View File
@@ -0,0 +1,295 @@
// 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 (front view)
glass_y0 = 7.85; // glass bottom edge from PCB bottom edge
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
/* [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 ----------
gx_c = glass_x0 + 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 + glass_y0 + 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() {
translate([disp_x0 + glass_x0, disp_raise + glass_y0, 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(); }
}