diff --git a/README.md b/README.md index 567faff..fd21902 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ home_assistant/packages/ el512_panel.yaml: helper sensors for the ESPHome pages tools/ import_still_alive.py, konosuba_sprites.py, steins_sprites.py, trek_sprites.py, yourname_sprites.py, render_pages.py (+ render/: PC renderer) +enclosure/ el512_frame.scad: 3D-printed insert for a 250x200x30 photo frame test/ unit tests (placeholder) docs/ datasheet and notes ``` diff --git a/enclosure/el512_frame.scad b/enclosure/el512_frame.scad new file mode 100644 index 0000000..880908e --- /dev/null +++ b/enclosure/el512_frame.scad @@ -0,0 +1,257 @@ +// Skeleton insert for a 250 x 200 x 30 mm photo frame: holds the Planar EL512.256-H +// near the top and a Wemos D1 mini ESP32 below it, visible through the glass. +// +// Panel dimensions are from the datasheet (docs/datasheet, Table 9, Fig. 7), which wins +// over anything else here: PCB 233 x 136 mm, depth 16.5 mm, four M3 insert nuts +// (2.5 mm deep) on a 222.4 x 124.0 mm grid, insert nut tops 8.5 mm behind the front. +// The panel is the bare version (no FRA/FRB mounting frame). +// +// Coordinates: X to the right and Y up as seen from the front, Z towards the viewer. +// The part prints as modelled: back face (z = 0) on the bed, no supports. +// +// Stack inside the photo frame, front to back: +// photo glass | gap | panel (16.5) | >= 5 mm air gap | truss plate | backboard +// The rim touches the glass and the plate touches the backboard, so the backboard +// clamps the whole insert in place. + +/* [Photo frame] */ +frame_w = 245; // inside width of the photo frame +frame_h = 194; // inside height +frame_depth = 30; // inside depth, glass included +glass_t = 2; // photo glass thickness +backboard_t = 3; // backboard thickness (keep it, but drill vent holes) +fit = 0.6; // total clearance per axis against the frame + +/* [Panel, datasheet Table 9 / Fig. 7] */ +pcb_w = 233; +pcb_h = 136; +panel_d = 16.5; // front of panel glass to tallest part on the back +hole_dx = 222.4; // insert nut grid +hole_dy = 124.0; +nut_top = 8.5; // front of panel to top of the insert nuts +insert_depth = 2.5; // M3 thread depth in the nuts +air_gap = 5; // recommended free space behind the panel +glass_gap = 0.5; // photo glass to panel front, so the EL glass is not pressed +top_margin = 10; // frame inside top edge to panel PCB top edge + +/* [Screws] */ +// The heads sit below the back face, so the back stays flat against the backboard. +head_type = "socket"; // [countersunk:ISO 10642 / DIN 7991 90 degree, socket:ISO 4762] +screw_len = 0; // nominal length (countersunk: overall, head included); 0 = longest that fits +screw_engage = 2; // thread engagement in the insert nut, < insert_depth +m3_clear = 3.4; +csk_d = 6.9; // countersink diameter (M3 head 6.72 max) +socket_d = 6.2; // counterbore for a socket head (5.5 max) +socket_h = 3.0; // socket head height +hole_slot = 1.5; // extra hole length in X to absorb measuring differences + +/* [Truss] */ +plate_t = 3; // plate thickness +wall = 2; // rim wall thickness +border = 7; // solid band around the edge, rim included +cell = 26; // triangle side length +strut = 3.5; // strut width +fillet = 1.2; // corner rounding inside the cells +standoff_d = 9; + +/* [D1 mini ESP32] */ +d1_l = 39.0; // along the USB axis +d1_w = 31.4; +d1_t = 1.2; // PCB thickness +d1_glass_gap = 7; // photo glass to the board's top face (ESP32 module is ~3.2 high) +d1_rot = 0; // 0: USB to the right, 180: USB to the left +d1_clear = 0.3; // clearance around the board +d1_ledge = 2; // seat width under the short ends (the pin rows run along the long sides) +d1_tab_w = 6; // snap tab width +d1_tab_t = 1.4; // snap tab thickness +d1_lip = 0.8; // how far the snap lip reaches over the board +d1_wall = 1.6; + +/* [Preview] */ +show_parts = true; // ghosts of the panel and the D1 mini (preview only) + +$fn = 48; +eps = 0.01; + +// ---------- derived ---------- +W = frame_w - fit; +H = frame_h - fit; +part_d = frame_depth - glass_t - backboard_t; // total insert depth + +panel_front = part_d - glass_gap; // z of the panel front +nut_z = panel_front - nut_top; // z of the insert nut tops +panel_back = panel_front - panel_d; // z of the tallest panel part +csk = head_type == "countersunk"; +// longest standard M3 length whose head stays below the back face +std_len = [6, 8, 10, 12, 14, 16, 18, 20, 25, 30]; +max_len = nut_z + screw_engage - (csk ? 0.3 : socket_h + 0.2); +fits = [for (l = std_len) if (l <= max_len) l]; +screw = screw_len > 0 ? screw_len : fits[len(fits) - 1]; +head_sink = nut_z + screw_engage - screw; // back face to the top of the screw head + +pcb_x0 = (W - pcb_w) / 2; +pcb_y1 = H - top_margin; +pcb_y0 = pcb_y1 - pcb_h; +pcb_cx = W / 2; +pcb_cy = (pcb_y0 + pcb_y1) / 2; + +holes = [for (sx = [-1, 1], sy = [-1, 1]) [pcb_cx + sx * hole_dx / 2, pcb_cy + sy * hole_dy / 2]]; + +d1_cx = W / 2; +d1_cy = (border + pcb_y0) / 2; +d1_top = part_d - d1_glass_gap; // z of the board's top face +d1_seat = d1_top - d1_t; // z of the board's bottom face +d1_in_l = d1_l + 2 * d1_clear; +d1_in_w = d1_w + 2 * d1_clear; + +assert(pcb_x0 >= wall + 1, "panel does not fit the frame width"); +assert(pcb_y0 - border >= d1_w + 2 * d1_wall + 4, "no room below the panel for the D1 mini"); +assert(panel_back - plate_t >= air_gap, "less than the recommended air gap behind the panel"); +assert(screw_engage < insert_depth, "screw would bottom out in the insert nut"); +assert(head_sink >= (csk ? 0.3 : socket_h + 0.2), + str("screw head would stick out of the back: use M3x", floor(max_len), " or shorter")); +assert(head_sink <= nut_z - 4, "screw_len too short for this stack"); + +echo(str("Insert depth ", part_d, " mm, air gap behind panel ", panel_back - plate_t, + " mm, standoff height ", nut_z - plate_t, " mm, use M3x", screw, " ", head_type, + " (head ", head_sink, " mm below the back face)")); + +// ---------- 2D ---------- +module outline() square([W, H]); + +module d1_footprint(grow = 0) { + translate([d1_cx, d1_cy]) rotate(d1_rot) + square([d1_in_l + 2 * (d1_tab_t + 1) + 2 * grow, d1_in_w + 2 * d1_wall + 2 * grow], center = true); +} + +// Triangle cells; a horizontal strut runs along the panel's bottom edge and a vertex +// sits on the centre line, so the pattern is symmetric. +module tri_cells() { + h = cell * sqrt(3) / 2; + x0 = W / 2 - cell * ceil(W / 2 / cell) - cell; + y0 = pcb_y0 - h * ceil(pcb_y0 / h) - h; + for (j = [0 : ceil(H / h) + 2], k = [0 : 2 * ceil(W / cell) + 4]) { + x = x0 + k * cell / 2; + y = y0 + j * h; + up = (k + j) % 2 == 0; + pts = up ? [[x, y], [x + cell, y], [x + cell / 2, y + h]] + : [[x, y + h], [x + cell, y + h], [x + cell / 2, y]]; + offset(r = fillet) offset(delta = -(strut / 2 + fillet)) polygon(pts); + } +} + +module plate_2d() { + difference() { + outline(); + // the opening drops slivers of cells clipped by the border and the pads + offset(r = 1.5) offset(delta = -1.5) difference() { + intersection() { + offset(delta = -border) outline(); + tri_cells(); + } + // solid pads under the standoffs and the D1 mini cradle + for (p = holes) translate(p) circle(d = standoff_d + 8); + d1_footprint(2); + } + } +} + +// ---------- 3D ---------- +module rim() { + linear_extrude(part_d) difference() { + outline(); + offset(delta = -wall) outline(); + } +} + +module standoff(p) { + translate(p) { + cylinder(d = standoff_d, h = nut_z); + cylinder(d1 = standoff_d + 6, d2 = standoff_d, h = plate_t + 3); // base fillet + } +} + +module slot() hull() for (s = [-1, 1]) translate([s * hole_slot / 2, 0, 0]) children(); + +// Slotted screw hole. Countersunk: the head's top face sits head_sink below the back +// face. Socket head: its bearing face sits head_sink deep, the head in a counterbore. +module screw_cut(p) translate(p) { + slot() translate([0, 0, -1]) cylinder(d = m3_clear, h = nut_z + 2); + if (csk) { + slot() translate([0, 0, -1]) cylinder(d = csk_d, h = head_sink + 1); + slot() translate([0, 0, head_sink - eps]) + cylinder(d1 = csk_d, d2 = m3_clear, h = (csk_d - m3_clear) / 2); // 90 degrees + } else { + slot() translate([0, 0, -1]) cylinder(d = socket_d, h = head_sink + 1); + } +} + +// Cradle for the D1 mini, board face up towards the glass. It rests on ledges under its +// short ends and two snap tabs at each end hold it down; the pin rows hang free over +// the hollow inside. The upper long wall has a slot for the wires to the panel. +module d1_cradle() { + zl = d1_top + d1_lip + 0.6; // top of the snap tabs + translate([d1_cx, d1_cy, 0]) rotate(d1_rot) { + difference() { + union() { + // long walls, flush with the board's top face + for (s = [-1, 1]) translate([-d1_in_l / 2, s > 0 ? d1_in_w / 2 : -d1_in_w / 2 - d1_wall, 0]) + cube([d1_in_l, d1_wall, d1_top]); + // seat ledges under the short ends + for (s = [-1, 1]) translate([s > 0 ? d1_in_l / 2 - d1_ledge : -d1_in_l / 2, -d1_in_w / 2, 0]) + cube([d1_ledge, d1_in_w, d1_seat]); + } + // wire slot towards the panel + translate([-10, d1_in_w / 2 - eps, plate_t]) cube([20, d1_wall + 2 * eps, d1_seat - plate_t - 2]); + } + // snap tabs, two per end, clear of the centred USB socket + for (sx = [-1, 1], sy = [-1, 1]) + translate([sx * (d1_in_l / 2 + 1 + d1_tab_t / 2), sy * (d1_in_w / 2 - d1_tab_w / 2), 0]) + rotate(sx > 0 ? 0 : 180) snap_tab(zl); + } +} + +// Tab standing outside the board edge, lip pointing to -X (towards the board). +// Printable without supports: the underside is a 45 degree chamfer across the 1 mm gap +// to the pocket, and only the catch itself (clearance + d1_lip) is a flat overhang. +// The tip has a short vertical face, so it does not print as a knife edge. +module snap_tab(zl) { + translate([-d1_tab_t / 2, -d1_tab_w / 2, 0]) cube([d1_tab_t, d1_tab_w, zl]); + gap = 1; // the tab stands 1 mm clear of the pocket + reach = gap + d1_clear + d1_lip; // tab face to lip tip + tip_h = 0.6; + translate([d1_tab_t / 2, d1_tab_w / 2, d1_top + 0.1]) rotate([90, 0, 0]) + linear_extrude(d1_tab_w) polygon([ + [0, -gap], [-d1_tab_t, -gap], // chamfer starts on the tab face + [-d1_tab_t - gap, 0], // 45 degrees across the gap + [-d1_tab_t - reach, 0], // flat catch over the board + [-d1_tab_t - reach, tip_h], // blunt tip + [-d1_tab_t, tip_h + reach], // 45 degree lead-in + [0, tip_h + reach]]); +} + +module frame() { + difference() { + union() { + linear_extrude(plate_t) plate_2d(); + rim(); + for (p = holes) standoff(p); + d1_cradle(); + } + for (p = holes) screw_cut(p); + } +} + +module ghosts() { + // panel: PCB and glass block, front at panel_front + color("dimgray", 0.35) translate([pcb_x0, pcb_y0, panel_back]) cube([pcb_w, pcb_h, panel_d]); + color("orange", 0.5) translate([pcb_cx - 97.55, pcb_cy - 48.75, panel_front - 0.2]) cube([195.1, 97.5, 0.2]); + // D1 mini + color("royalblue", 0.5) translate([d1_cx, d1_cy, d1_seat]) rotate(d1_rot) + translate([-d1_l / 2, -d1_w / 2, 0]) cube([d1_l, d1_w, d1_t]); + // photo glass + color("lightblue", 0.15) translate([0, 0, part_d]) cube([W, H, glass_t]); +} + +frame(); +if (show_parts && $preview) %ghosts();