diff --git a/CLAUDE.md b/CLAUDE.md index d25c68d..6d96ec2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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, wiring and the CG-ROM tables (section 6). Check it before changing the 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 component from git; for local work, switch to the commented `type: local, path: ../components` source. diff --git a/README.md b/README.md index 6e3a604..a3511af 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ components/samsung_vfd/ the component (Python schema + C++) examples/samsung-vfd.yaml full example device config examples/secrets.yaml.example 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 ``` diff --git a/case/README.md b/case/README.md new file mode 100644 index 0000000..b95b313 --- /dev/null +++ b/case/README.md @@ -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. + +

+ Case preview + Cross-section: display on the bezel, board on the base +

+ +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. diff --git a/case/preview.png b/case/preview.png new file mode 100644 index 0000000..a342b59 Binary files /dev/null and b/case/preview.png differ diff --git a/case/section.png b/case/section.png new file mode 100644 index 0000000..c0e6f28 Binary files /dev/null and b/case/section.png differ diff --git a/case/stl/base.stl b/case/stl/base.stl new file mode 100644 index 0000000..c39433a Binary files /dev/null and b/case/stl/base.stl differ diff --git a/case/stl/bezel.stl b/case/stl/bezel.stl new file mode 100644 index 0000000..3267851 Binary files /dev/null and b/case/stl/bezel.stl differ diff --git a/case/stl/body.stl b/case/stl/body.stl new file mode 100644 index 0000000..ee116a6 Binary files /dev/null and b/case/stl/body.stl differ diff --git a/case/vfd-case.scad b/case/vfd-case.scad new file mode 100644 index 0000000..f657615 --- /dev/null +++ b/case/vfd-case.scad @@ -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(); } +}