saltylab-firmware/chassis/charging_dock_505.scad
sl-mechanical b0c2b5564d feat: Add Issue #505 CAD - 24V Charging Dock OpenSCAD Models
CAD implementation files for Issue #505 (24V charging dock upgrade):

- charging_dock_505.scad: Main dock assembly
  * Base plate: 340×320×12 mm (enlarged for 240W PSU)
  * Back wall: 250×85×10 mm (pogo pin housing, LED bezel recess)
  * V-guide rails: 100mm deep, self-centering funnel (print 2×)
  * ArUco marker frame: ID 42 (DICT_4X4_250), 15cm mast
  * PSU bracket: Sized for Mean Well IRM-240-24 (210×108×56 mm)
  * LED bezel: 4× status indicators (SEARCHING/ALIGNED/CHARGING/FULL)

- charging_dock_receiver_505.scad: Robot-side receiver variants
  * Lab receiver: Stem collar mount (SaltyLab)
  * Rover receiver: Deck flange mount (SaltyRover)
  * Tank receiver: Skid plate mount + extended nose (SaltyTank)
  * Common contact geometry: 20mm CL-to-CL brass pads, V-nose guide
  * Wire bore: 3mm (supports 12 AWG charging wires)

Key changes from Issue #159 (5V):
- PSU dimensions: 63×45×28 mm → 210×108×56 mm
- Base/bracket enlarged accordingly
- ArUco ID: 0 → 42
- Contact geometry unchanged (compatible with Issue #159 receivers)
- Pogo pins, V-guides, LED circuit identical

Files ready for:
- STL export via OpenSCAD render commands
- 3D printing (PETG recommended)
- Assembly integration with docking node (#489)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-06 11:44:28 -05:00

532 lines
24 KiB
OpenSCAD
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ============================================================
// charging_dock_505.scad — 24V Charging Dock Station
// Issue: #505 Agent: sl-mechanical Date: 2026-03-06
// ============================================================
//
// 24V upgraded dock (forked from Issue #159 5V design).
// Robot drives forward into V-guide funnel; spring-loaded pogo pins
// make contact with the robot receiver plate (charging_dock_receiver.scad).
//
// Power: 24 V / 10 A (240 W) via 2× high-current pogo pins (+/-)
// Alignment tolerance: ±20 mm lateral (V-guide funnels to centre)
//
// Dock architecture (top view):
//
// ┌─────────────────────────────────┐ ← back wall (robot stops here)
// │ PSU shelf │
// │ [PSU] [LED ×4] │
// │ [POGO+][POGO-] │ ← pogo face (robot contact)
// └────\ /────────┘
// \ V-guide rails /
// \ /
// ╲ ← dock entry, ±20 mm funnel
//
// Components (this file):
// Part A — dock_base() weighted base plate with ballast pockets
// Part B — back_wall() upright back panel + pogo housing + LED bezel
// Part C — guide_rail(side) V-funnel guide rail, L/R (print 2×)
// Part D — aruco_mount() ArUco marker frame at dock entrance
// Part E — psu_bracket() PSU retention bracket (rear of base)
// Part F — led_bezel() 4-LED status bezel
//
// Robot-side receiver → see charging_dock_receiver.scad
//
// Coordinate system:
// Z = 0 at dock floor (base plate top face)
// Y = 0 at back wall front face (robot approaches from +Y)
// X = 0 at dock centre
// Robot drives in -Y direction to dock.
//
// RENDER options:
// "assembly" full dock preview (default)
// "base_stl" base plate (print 1×)
// "back_wall_stl" back wall + pogo housing (print 1×)
// "guide_rail_stl" V-guide rail (print 2×, mirror for R side)
// "aruco_mount_stl" ArUco marker frame (print 1×)
// "psu_bracket_stl" PSU mounting bracket (print 1×)
// "led_bezel_stl" LED status bezel (print 1×)
//
// Export commands (Issue #505 24V variant):
// openscad charging_dock_505.scad -D 'RENDER="base_stl"' -o dock_505_base.stl
// openscad charging_dock_505.scad -D 'RENDER="back_wall_stl"' -o dock_505_back_wall.stl
// openscad charging_dock_505.scad -D 'RENDER="guide_rail_stl"' -o dock_505_guide_rail.stl
// openscad charging_dock_505.scad -D 'RENDER="aruco_mount_stl"' -o dock_505_aruco_mount.stl
// openscad charging_dock_505.scad -D 'RENDER="psu_bracket_stl"' -o dock_505_psu_bracket.stl
// openscad charging_dock_505.scad -D 'RENDER="led_bezel_stl"' -o dock_505_led_bezel.stl
// ============================================================
$fn = 64;
e = 0.01;
// ── Base plate dimensions ─────────────────────────────────────────────────────
// NOTE: Enlarged for 24V PSU (IRM-240-24: 210×108×56 mm vs. IRM-30-5: 63×45×28 mm)
BASE_W = 340.0; // base width (X) — increased for larger PSU bracket
BASE_D = 320.0; // base depth (Y, extends behind and in front of back wall)
BASE_T = 12.0; // base thickness
BASE_R = 10.0; // corner radius
// Ballast pockets (for steel hex bar / bolt weights):
// 4× pockets in base underside, accept M20 hex nuts (30 mm AF) stacked
BALLAST_N = 4;
BALLAST_W = 32.0; // pocket width (hex nut AF + 2 mm)
BALLAST_D = 32.0; // pocket depth
BALLAST_T = 8.0; // pocket depth (≤ BASE_T/2)
BALLAST_INSET_X = 50.0;
BALLAST_INSET_Y = 40.0;
// Floor bolt holes (M8, for bolting dock to bench/floor — optional)
FLOOR_BOLT_D = 8.5;
FLOOR_BOLT_INSET_X = 30.0;
FLOOR_BOLT_INSET_Y = 25.0;
// ── Back wall (upright panel) ─────────────────────────────────────────────────
WALL_W = 250.0; // wall width (X) — same as guide entry span
WALL_H = 85.0; // wall height (Z)
WALL_T = 10.0; // wall thickness (Y)
// Back wall Y position relative to base rear edge
// Wall sits at Y=0 (its front face); base extends behind it (-Y) and in front (+Y)
BASE_REAR_Y = -80.0; // base rear edge Y coordinate
// ── Pogo pin housing (in back wall front face) ────────────────────────────────
// High-current pogo pins: Ø5.5 mm body, 20 mm long (compressed), 4 mm spring travel
// Rated 5 A each; 2× pins for +/- power
POGO_D = 5.5; // pogo pin body OD
POGO_BORE_D = 5.7; // bore diameter (0.2 mm clearance)
POGO_L = 20.0; // pogo full length (uncompressed)
POGO_TRAVEL = 4.0; // spring travel
POGO_FLANGE_D = 8.0; // pogo flange / retention shoulder OD
POGO_FLANGE_T = 1.5; // flange thickness
POGO_SPACING = 20.0; // CL-to-CL spacing between + and - pins
POGO_Z = 35.0; // pogo CL height above dock floor
POGO_PROTRUDE = 8.0; // pogo tip protrusion beyond wall face (uncompressed)
// Wiring channel behind pogo (runs down to base)
WIRE_CH_W = 8.0;
WIRE_CH_H = POGO_Z + 5;
// ── LED bezel (4 status LEDs in back wall, above pogo pins) ───────────────────
// LED order (left to right): Searching | Aligned | Charging | Full
// Colours (suggested): Red | Yellow | Blue | Green
LED_D = 5.0; // 5 mm through-hole LED
LED_BORE_D = 5.2; // bore diameter
LED_BEZEL_W = 80.0; // bezel plate width
LED_BEZEL_H = 18.0; // bezel plate height
LED_BEZEL_T = 4.0; // bezel plate thickness
LED_SPACING = 16.0; // LED centre-to-centre
LED_Z = 65.0; // LED centre height above floor
LED_INSET_D = 2.0; // LED recess depth (LED body recessed for protection)
// ── V-guide rails ─────────────────────────────────────────────────────────────
// Robot receiver width (contact block): 30 mm.
// Alignment tolerance: ±20 mm → entry gap = 30 + 2×20 = 70 mm.
// Guide rail tapers from 70 mm entry (at Y = GUIDE_L) to 30 mm exit (at Y=0).
// Each rail is a wedge-shaped wall.
GUIDE_L = 100.0; // guide rail length (Y depth, from back wall)
GUIDE_H = 50.0; // guide rail height (Z)
GUIDE_T = 8.0; // guide rail wall thickness
RECV_W = 30.0; // robot receiver contact block width
ENTRY_GAP = 70.0; // guide entry gap (= RECV_W + 2×20 mm tolerance)
EXIT_GAP = RECV_W + 2.0; // guide exit gap (2 mm clearance on each side)
// Derived: half-gap at entry = 35 mm, at exit = 16 mm; taper = 19 mm over 100 mm
// Half-angle = atan(19/100) ≈ 10.8° — gentle enough for reliable self-alignment
// ── ArUco marker mount ────────────────────────────────────────────────────────
// Mounted at dock entry arch (forward of guide rails), tilted 15° back.
// Robot camera acquires marker for coarse approach alignment.
// ArUco marker ID 42 (DICT_4X4_250), 100×100 mm (printed/laminated on paper).
ARUCO_MARKER_W = 100.0;
ARUCO_MARKER_H = 100.0;
ARUCO_FRAME_T = 3.0; // frame plate thickness
ARUCO_FRAME_BDR = 10.0; // frame border around marker
ARUCO_SLOT_T = 1.5; // marker slip-in slot depth
ARUCO_MAST_H = 95.0; // mast height above base (centres marker at camera height)
ARUCO_MAST_W = 10.0;
ARUCO_TILT = 15.0; // backward tilt (degrees) — faces approaching robot
ARUCO_Y = GUIDE_L + 60; // mast Y position (in front of guide entry)
// ── PSU bracket ───────────────────────────────────────────────────────────────
// Mean Well IRM-240-24 (24V 10A 240W): 210×108×56 mm body — Issue #505 upgrade
// Bracket sits behind back wall, on base plate.
PSU_W = 220.0; // bracket internal width (+5 mm clearance per side for 210 mm PSU)
PSU_D = 118.0; // bracket internal depth (+5 mm clearance per side for 108 mm PSU)
PSU_H = 66.0; // bracket internal height (+5 mm top clearance for 56 mm PSU + ventilation)
PSU_T = 4.0; // bracket wall thickness (thicker for larger PSU mass)
PSU_Y = BASE_REAR_Y + PSU_D/2 + PSU_T + 10; // PSU Y centre
// ── Fasteners ─────────────────────────────────────────────────────────────────
M3_D = 3.3;
M4_D = 4.3;
M5_D = 5.3;
M8_D = 8.5;
// ============================================================
// RENDER DISPATCH
// ============================================================
RENDER = "assembly";
if (RENDER == "assembly") assembly();
else if (RENDER == "base_stl") dock_base();
else if (RENDER == "back_wall_stl") back_wall();
else if (RENDER == "guide_rail_stl") guide_rail("left");
else if (RENDER == "aruco_mount_stl") aruco_mount();
else if (RENDER == "psu_bracket_stl") psu_bracket();
else if (RENDER == "led_bezel_stl") led_bezel();
// ============================================================
// ASSEMBLY PREVIEW
// ============================================================
module assembly() {
// Base plate
color("SaddleBrown", 0.85) dock_base();
// Back wall
color("Sienna", 0.85)
translate([0, 0, BASE_T])
back_wall();
// Left guide rail
color("Peru", 0.85)
translate([0, 0, BASE_T])
guide_rail("left");
// Right guide rail (mirror in X)
color("Peru", 0.85)
translate([0, 0, BASE_T])
mirror([1, 0, 0])
guide_rail("left");
// ArUco mount
color("DimGray", 0.85)
translate([0, 0, BASE_T])
aruco_mount();
// PSU bracket
color("DarkSlateGray", 0.80)
translate([0, PSU_Y, BASE_T])
psu_bracket();
// LED bezel
color("LightGray", 0.90)
translate([0, -WALL_T/2, BASE_T + LED_Z])
led_bezel();
// Ghost robot receiver approaching from +Y
%color("SteelBlue", 0.25)
translate([0, GUIDE_L + 30, BASE_T + POGO_Z])
cube([RECV_W, 20, 8], center = true);
// Ghost pogo pins
for (px = [-POGO_SPACING/2, POGO_SPACING/2])
%color("Gold", 0.60)
translate([px, -POGO_PROTRUDE, BASE_T + POGO_Z])
rotate([90, 0, 0])
cylinder(d = POGO_D, h = POGO_L);
}
// ============================================================
// PART A — DOCK BASE PLATE
// ============================================================
module dock_base() {
difference() {
// ── Main base block (rounded rect) ──────────────────────────
linear_extrude(BASE_T)
minkowski() {
square([BASE_W - 2*BASE_R,
BASE_D - 2*BASE_R], center = true);
circle(r = BASE_R);
}
// ── Ballast pockets (underside) ──────────────────────────────
// 4× pockets: 2 front, 2 rear
for (bx = [-1, 1])
for (by = [-1, 1])
translate([bx * (BASE_W/2 - BALLAST_INSET_X),
by * (BASE_D/2 - BALLAST_INSET_Y),
-e])
cube([BALLAST_W, BALLAST_D, BALLAST_T + e], center = true);
// ── Floor bolt holes (M8, 4 corners) ────────────────────────
for (bx = [-1, 1])
for (by = [-1, 1])
translate([bx * (BASE_W/2 - FLOOR_BOLT_INSET_X),
by * (BASE_D/2 - FLOOR_BOLT_INSET_Y), -e])
cylinder(d = FLOOR_BOLT_D, h = BASE_T + 2*e);
// ── Back wall attachment slots (M4, top face) ─────────────────
for (bx = [-WALL_W/2 + 30, 0, WALL_W/2 - 30])
translate([bx, -BASE_D/4, BASE_T - 3])
cylinder(d = M4_D, h = 4 + e);
// ── Guide rail attachment holes (M4) ──────────────────────────
for (side = [-1, 1])
for (gy = [20, GUIDE_L - 20])
translate([side * (EXIT_GAP/2 + GUIDE_T/2), gy, BASE_T - 3])
cylinder(d = M4_D, h = 4 + e);
// ── Cable routing slot (from pogo wires to PSU, through base) ─
translate([0, -WALL_T - 5, -e])
cube([WIRE_CH_W, 15, BASE_T + 2*e], center = true);
// ── Anti-skid texture (front face chamfer) ───────────────────
// Chamfer front-bottom edge for easy robot approach
translate([0, BASE_D/2 + e, -e])
rotate([45, 0, 0])
cube([BASE_W + 2*e, 5, 5], center = true);
}
}
// ============================================================
// PART B — BACK WALL (upright panel)
// ============================================================
module back_wall() {
difference() {
union() {
// ── Wall slab ────────────────────────────────────────────
translate([-WALL_W/2, -WALL_T, 0])
cube([WALL_W, WALL_T, WALL_H]);
// ── Pogo pin housing bosses (front face) ─────────────────
for (px = [-POGO_SPACING/2, POGO_SPACING/2])
translate([px, -WALL_T, POGO_Z])
rotate([90, 0, 0])
cylinder(d = POGO_FLANGE_D + 6,
h = POGO_PROTRUDE);
// ── Wiring channel reinforcement (inside wall face) ───────
translate([-WIRE_CH_W/2 - 2, -WALL_T, 0])
cube([WIRE_CH_W + 4, 4, WIRE_CH_H]);
}
// ── Pogo pin bores (through wall into housing boss) ───────────
for (px = [-POGO_SPACING/2, POGO_SPACING/2])
translate([px, POGO_PROTRUDE + e, POGO_Z])
rotate([90, 0, 0]) {
// Main bore (full depth through wall + boss)
cylinder(d = POGO_BORE_D,
h = WALL_T + POGO_PROTRUDE + 2*e);
// Flange shoulder counterbore (retains pogo from pulling out)
translate([0, 0, WALL_T + POGO_PROTRUDE - POGO_FLANGE_T - 1])
cylinder(d = POGO_FLANGE_D + 0.4,
h = POGO_FLANGE_T + 2);
}
// ── Wiring channel (vertical slot, inside face → base cable hole) ─
translate([-WIRE_CH_W/2, 0 + e, 0])
cube([WIRE_CH_W, WALL_T/2, WIRE_CH_H]);
// ── LED bezel recess (in front face, above pogo) ──────────────
translate([-LED_BEZEL_W/2, -LED_BEZEL_T, LED_Z - LED_BEZEL_H/2])
cube([LED_BEZEL_W, LED_BEZEL_T + e, LED_BEZEL_H]);
// ── M4 base attachment bores (3 through bottom of wall) ───────
for (bx = [-WALL_W/2 + 30, 0, WALL_W/2 - 30])
translate([bx, -WALL_T/2, -e])
cylinder(d = M4_D, h = 8 + e);
// ── Cable tie slots (in wall body, for neat wire routing) ─────
for (cz = [15, POGO_Z - 15])
translate([WIRE_CH_W/2 + 3, -WALL_T/2, cz])
cube([4, WALL_T + 2*e, 3], center = true);
// ── Lightening cutout (rear face pocket) ──────────────────────
translate([-WALL_W/2 + 40, 0, 20])
cube([WALL_W - 80, WALL_T/2 + e, WALL_H - 30]);
}
}
// ============================================================
// PART C — V-GUIDE RAIL
// ============================================================
// Print 2×; mirror in X for right side.
// Rail tapers from ENTRY_GAP/2 (at Y=GUIDE_L) to EXIT_GAP/2 (at Y=0).
// Inner (guiding) face is angled; outer face is vertical.
module guide_rail(side = "left") {
// Inner face X at back wall = EXIT_GAP/2
// Inner face X at entry = ENTRY_GAP/2
x_back = EXIT_GAP/2; // 16 mm
x_entry = ENTRY_GAP/2; // 35 mm
difference() {
union() {
// ── Main wedge body ─────────────────────────────────────
// Hull between two rectangles: narrow at Y=0, wide at Y=GUIDE_L
hull() {
// Back end (at Y=0, flush with back wall)
translate([x_back, 0, 0])
cube([GUIDE_T, e, GUIDE_H]);
// Entry end (at Y=GUIDE_L)
translate([x_entry, GUIDE_L, 0])
cube([GUIDE_T, e, GUIDE_H]);
}
// ── Entry flare (chamfered lip at guide entry for bump-entry) ─
hull() {
translate([x_entry, GUIDE_L, 0])
cube([GUIDE_T, e, GUIDE_H]);
translate([x_entry + 15, GUIDE_L + 20, 0])
cube([GUIDE_T, e, GUIDE_H * 0.6]);
}
}
// ── M4 base attachment bores ─────────────────────────────────
for (gy = [20, GUIDE_L - 20])
translate([x_back + GUIDE_T/2, gy, -e])
cylinder(d = M4_D, h = 8 + e);
// ── Chamfer on inner top corner (smooth robot entry) ─────────
translate([x_back - e, -e, GUIDE_H - 5])
rotate([0, -45, 0])
cube([8, GUIDE_L + 30, 8]);
}
}
// ============================================================
// PART D — ArUco MARKER MOUNT
// ============================================================
// Free-standing mast at dock entry. Mounts to base plate.
// Marker face tilted 15° toward approaching robot.
// Accepts 100×100 mm printed/laminated paper marker in slot.
module aruco_mount() {
frame_w = ARUCO_MARKER_W + 2*ARUCO_FRAME_BDR;
frame_h = ARUCO_MARKER_H + 2*ARUCO_FRAME_BDR;
mast_y = ARUCO_Y;
union() {
// ── Mast column ───────────────────────────────────────────────
translate([-ARUCO_MAST_W/2, mast_y - ARUCO_MAST_W/2, 0])
cube([ARUCO_MAST_W, ARUCO_MAST_W, ARUCO_MAST_H]);
// ── Marker frame (tilted back ARUCO_TILT°) ────────────────────
translate([0, mast_y, ARUCO_MAST_H])
rotate([-ARUCO_TILT, 0, 0]) {
difference() {
// Frame plate
translate([-frame_w/2, -ARUCO_FRAME_T, -frame_h/2])
cube([frame_w, ARUCO_FRAME_T, frame_h]);
// Marker window (cutout for marker visibility)
translate([-ARUCO_MARKER_W/2, -ARUCO_FRAME_T - e,
-ARUCO_MARKER_H/2])
cube([ARUCO_MARKER_W,
ARUCO_FRAME_T + 2*e,
ARUCO_MARKER_H]);
// Marker slip-in slot (insert from side)
translate([-frame_w/2 - e,
-ARUCO_SLOT_T - 0.3,
-ARUCO_MARKER_H/2])
cube([frame_w + 2*e,
ARUCO_SLOT_T + 0.3,
ARUCO_MARKER_H]);
}
}
// ── Mast base foot (M4 bolts to dock base) ────────────────────
difference() {
translate([-20, mast_y - 20, 0])
cube([40, 40, 5]);
for (fx = [-12, 12]) for (fy = [-12, 12])
translate([fx, mast_y + fy, -e])
cylinder(d = M4_D, h = 6 + e);
}
}
}
// ============================================================
// PART E — PSU BRACKET
// ============================================================
// Open-top retention bracket for PSU module.
// PSU slides in from top; 2× M3 straps or cable ties retain it.
// Bracket bolts to base plate via 4× M4 screws.
module psu_bracket() {
difference() {
union() {
// ── Outer bracket box (open top) ─────────────────────────
_box_open_top(PSU_W + 2*PSU_T,
PSU_D + 2*PSU_T,
PSU_H + PSU_T);
// ── Base flange ──────────────────────────────────────────
translate([-(PSU_W/2 + PSU_T + 8),
-(PSU_D/2 + PSU_T + 8), -PSU_T])
cube([PSU_W + 2*PSU_T + 16,
PSU_D + 2*PSU_T + 16, PSU_T]);
}
// ── PSU cavity ───────────────────────────────────────────────
translate([0, 0, PSU_T])
cube([PSU_W, PSU_D, PSU_H + e], center = true);
// ── Ventilation slots (sides) ─────────────────────────────────
for (a = [0, 90, 180, 270])
rotate([0, 0, a])
translate([0, (PSU_D/2 + PSU_T)/2, PSU_H/2 + PSU_T])
for (sz = [-PSU_H/4, 0, PSU_H/4])
translate([0, 0, sz])
cube([PSU_W * 0.5, PSU_T + 2*e, 5],
center = true);
// ── Cable exit slot (bottom) ──────────────────────────────────
translate([0, 0, -e])
cube([15, PSU_D + 2*PSU_T + 2*e, PSU_T + 2*e],
center = true);
// ── Base flange M4 bolts ──────────────────────────────────────
for (fx = [-1, 1]) for (fy = [-1, 1])
translate([fx * (PSU_W/2 + PSU_T + 4),
fy * (PSU_D/2 + PSU_T + 4),
-PSU_T - e])
cylinder(d = M4_D, h = PSU_T + 2*e);
// ── Cable tie slots ───────────────────────────────────────────
for (sz = [PSU_H/3, 2*PSU_H/3])
translate([0, 0, PSU_T + sz])
cube([PSU_W + 2*PSU_T + 2*e, 4, 4], center = true);
}
}
module _box_open_top(w, d, h) {
difference() {
cube([w, d, h], center = true);
translate([0, 0, PSU_T + e])
cube([w - 2*PSU_T, d - 2*PSU_T, h], center = true);
}
}
// ============================================================
// PART F — LED STATUS BEZEL
// ============================================================
// 4 × 5 mm LEDs in a row. Press-fits into recess in back wall.
// LED labels (L→R): SEARCHING | ALIGNED | CHARGING | FULL
// Suggested colours: Red | Yellow | Blue | Green
module led_bezel() {
difference() {
// Bezel plate
cube([LED_BEZEL_W, LED_BEZEL_T, LED_BEZEL_H], center = true);
// 4× LED bores
for (i = [-1.5, -0.5, 0.5, 1.5])
translate([i * LED_SPACING, -LED_BEZEL_T - e, 0])
rotate([90, 0, 0]) {
// LED body bore (recess, not through)
cylinder(d = LED_BORE_D + 1,
h = LED_INSET_D + e);
// LED pin bore (through bezel)
translate([0, 0, LED_INSET_D])
cylinder(d = LED_BORE_D,
h = LED_BEZEL_T + 2*e);
}
// Label recesses between LEDs (for colour-dot stickers or printed inserts)
for (i = [-1.5, -0.5, 0.5, 1.5])
translate([i * LED_SPACING, LED_BEZEL_T/2, LED_BEZEL_H/2 - 3])
cube([LED_SPACING - 3, 1 + e, 5], center = true);
// M3 mounting holes (2× into back wall)
for (mx = [-LED_BEZEL_W/2 + 6, LED_BEZEL_W/2 - 6])
translate([mx, -LED_BEZEL_T - e, 0])
rotate([90, 0, 0])
cylinder(d = M3_D, h = LED_BEZEL_T + 2*e);
}
}