fix: redo camera node case as upright enclosure
CI/CD / lint-and-typecheck (pull_request) Successful in 7s
CI/CD / test (pull_request) Successful in 9m27s
CI/CD / build (pull_request) Failing after 4m51s
CI/CD / deploy (pull_request) Has been skipped

This commit is contained in:
2026-05-23 12:42:01 +00:00
parent 4487f0e0a4
commit bbc6b1ea05
6 changed files with 97955 additions and 74098 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+106 -107
View File
@@ -1,33 +1,34 @@
// RemoteRig camera node case v4
// Parametric OpenSCAD prototype for a status-panel camera node enclosure.
// Units: millimeters. Coordinate system: X=width, Y=depth/front-back, Z=height.
// Upright appliance-style OpenSCAD prototype for a strap-mounted camera node.
// Units: millimeters. Coordinate system: X=width, Y=depth/front-back, Z=height.
// Front/service lid is on the -Y face. Rear strap channels are on the +Y face.
//
// v4 visual direction: tall/upright appliance/control box matching the original
// reference image, replacing the rejected wide, low generic electronics box.
// Nominal body: 56 W x 36 D x 82 H mm; with low rear strap bridges ~40 D.
//
// Prototype assumptions to confirm against purchased parts:
// - 1.3 inch OLED module/window opening: 31 x 16 mm visible window, 35 x 19 mm panel clearance.
// - 1.3 inch OLED module/window opening: 31 x 16 mm visible window, 37 x 22 mm panel recess.
// - Small rocker switch cutout: 13 x 19 mm rectangular snap-in opening.
// - LEDs: two 3 mm panel LEDs (PWR + RGB STAT) with 3.2 mm holes.
// - Boards: ESP32-C3 Super Mini 22.5 x 18 mm, ESP-01S 24.7 x 14.3 x 12 mm.
$fn = 48;
$fn = 56;
// ----- Main enclosure parameters -----
case_w = 68; // external width; room for OLED + LEDs + switch
case_d = 42; // external depth; board stack + wiring clearance
case_h = 32; // external height
case_w = 56; // upright appliance-style external width
case_d = 36; // depth for module stack + wiring clearance
case_h = 82; // tall vertical appliance-style height
wall = 2.2;
corner_r = 3.0;
corner_r = 4.0;
front_recess_d = 2.0; // lid sits in this front pocket, nominally flush
lid_clearance = 0.35;
lid_w = case_w - 8;
lid_h = case_h - 6;
lid_w = case_w - 8; // nearly full-height/front-width inset panel
lid_h = case_h - 8;
lid_t = 2.0;
lid_lip_t = 1.2; // locating lip protrudes inside service opening
lid_lip_inset = 2.1;
// Central body opening avoids the four corner screw bosses. Separate local
// clearances below handle the OLED/switch bodies without cutting away bosses.
service_opening_w = 46.0;
service_opening_h = 11.5;
service_opening_w = lid_w - 10.0;
service_opening_h = lid_h - 16.0;
// Hardware
screw_d = 2.4; // M2 self-tapping / pilot; confirm hardware
@@ -36,29 +37,29 @@ boss_d = 6.0;
boss_len = 8.0;
// Front panel components
// OLED values are intentionally conservative prototype placeholders.
oled_window_w = 31.0;
oled_window_h = 16.0;
oled_bezel_w = 35.0; // engraved/recessed visual outline around window
oled_bezel_h = 19.0;
oled_bezel_w = 37.0; // shallow recessed visual outline around window
oled_bezel_h = 22.0;
oled_z = 53.0; // upper third, clear of top screw counterbores
led_hole_d = 3.2; // 3 mm LED clearance
rocker_w = 13.0; // prototype cutout; measure purchased rocker
rocker_h = 19.0;
vent_slot_w = 7.0;
vent_slot_h = 1.7;
front_slot_w = 34.0; // two long rounded horizontal slots near lower front
front_slot_h = 3.2;
// Rear reusable cloth zip-tie / Velcro strap channels
strap_bridge_w = case_w - 12;
strap_bridge_h = 7.5; // outer raised tab height
// Rear reusable cloth zip-tie / Velcro strap channels: low-profile external loops.
strap_bridge_w = case_w - 14;
strap_bridge_h = 6.5; // outer raised rail height
strap_slot_h = 4.2; // strap thickness opening; adjust for cloth ties
strap_gap_y = 3.2; // clearance behind bridge for strap material
strap_bridge_y = strap_gap_y + 2.2;
strap_z1 = 8.5;
strap_z2 = 23.5;
strap_gap_y = 2.4; // clearance behind bridge for strap material
strap_bridge_y = strap_gap_y + 2.4;
strap_z1 = 23.0;
strap_z2 = 50.0;
// Cable exit
cable_notch_w = 13.0;
cable_notch_h = 6.0;
cable_notch_h = 6.5;
// ----- Utility geometry -----
module rounded_box(size=[10,10,10], r=2, center_xy=true) {
@@ -68,6 +69,14 @@ module rounded_box(size=[10,10,10], r=2, center_xy=true) {
square([size[0]-2*r, size[1]-2*r], center=center_xy);
}
module xz_rounded_prism(w, d, h, r=2) {
// Rounded rectangle in the visible X/Z plane, extruded through Y.
rotate([-90,0,0])
linear_extrude(height=d, center=true)
offset(r=r)
square([w-2*r, h-2*r], center=true);
}
module y_cylinder(d, h, center=true) {
rotate([90,0,0]) cylinder(d=d, h=h, center=center);
}
@@ -81,17 +90,23 @@ module screw_boss(x, z) {
}
module strap_bridge(zc) {
// Raised rear bridge with a smooth-ish strap tunnel through it.
// It intentionally overlaps the main shell by ~2.2 mm so the STL is a
// single printable body, not separate coincident strap-tab volumes.
translate([0, case_d/2 + strap_bridge_y/2 - 2.2, zc - strap_bridge_h/2])
difference() {
rounded_box([strap_bridge_w, strap_bridge_y, strap_bridge_h], r=2.0);
// Through-slot for the cloth tie. It cuts all the way through Y,
// while leaving side posts that tie the upper/lower rails together.
// Matching rear-wall cutouts are made in camera_node_body_v4().
translate([0, 0, (strap_bridge_h - strap_slot_h)/2])
rounded_box([strap_bridge_w - 8.0, strap_bridge_y + 0.8, strap_slot_h], r=1.2);
// Low external rear belt-loop bridge for cloth ties/Velcro.
// The slot is entirely behind the rear wall so it does not expose the
// electronics cavity; a small overlap keeps the bridge connected to shell.
bridge_overlap_y = 0.8;
translate([0, case_d/2 + strap_bridge_y/2 - bridge_overlap_y, zc - strap_bridge_h/2])
union() {
difference() {
rounded_box([strap_bridge_w, strap_bridge_y, strap_bridge_h], r=1.8);
translate([0, -strap_bridge_y/2 + bridge_overlap_y + 0.05 + (strap_bridge_y + 1.0)/2,
(strap_bridge_h - strap_slot_h)/2])
rounded_box([strap_bridge_w - 9.0, strap_bridge_y + 1.0, strap_slot_h], r=1.1);
}
// Rear-wall weld pad: mechanically joins the external bridge to the
// shell while keeping the belt slot outside the electronics cavity.
translate([0, -strap_bridge_y/2 + bridge_overlap_y, strap_bridge_h/2])
cube([strap_bridge_w, 0.9, strap_bridge_h], center=true);
}
}
@@ -101,52 +116,37 @@ module camera_node_body_v4() {
union() {
difference() {
union() {
// Outer shell.
translate([0,0,0]) rounded_box([case_w, case_d, case_h], r=corner_r);
// Upright outer shell with softened appliance-like corners.
rounded_box([case_w, case_d, case_h], r=corner_r);
// Rear cloth zip-tie / Velcro strap bridges.
// Rear cloth zip-tie / Velcro strap bridges kept flat/quiet.
strap_bridge(strap_z1);
strap_bridge(strap_z2);
}
// Front recessed lid pocket (shallow, so the lid can sit flush).
translate([0, -case_d/2 - 0.2, case_h/2])
cube([lid_w + lid_clearance, front_recess_d + 0.3, lid_h + lid_clearance], center=true);
// Full-height front recessed lid pocket, like the green reference panel.
translate([0, -case_d/2 + front_recess_d/2, case_h/2])
cube([lid_w + lid_clearance, front_recess_d + 0.4, lid_h + lid_clearance], center=true);
// Through service opening behind the lid, leaving a recessed ledge/frame.
service_depth = front_recess_d + wall + 2.0;
translate([0, -case_d/2 + service_depth/2, case_h/2])
cube([service_opening_w, service_depth + 0.4, service_opening_h], center=true);
// Through service opening behind the lid, leaving a strong inset frame.
service_depth = front_recess_d + wall + 2.0;
translate([0, -case_d/2 + service_depth/2, case_h/2])
xz_rounded_prism(service_opening_w, service_depth + 0.4, service_opening_h, r=2.0);
// Local front clearances for the OLED module and rocker-switch body.
// These keep the screw-boss corner material intact while still leaving
// room for front-panel components and wiring to enter the body cavity.
translate([0, -case_d/2 + service_depth/2, (case_h - lid_h)/2 + lid_h - 9.5])
cube([oled_bezel_w + 2.0, service_depth + 0.4, oled_bezel_h + 3.0], center=true);
translate([15.5, -case_d/2 + service_depth/2, (case_h - lid_h)/2 + 12.0])
cube([rocker_w + 3.0, service_depth + 0.4, rocker_h + 3.0], center=true);
// Interior electronics cavity: ESP32-C3 Super Mini + ESP-01S plus wiring/service clearance.
cavity_d = case_d - front_recess_d - 2*wall;
translate([0, -case_d/2 + front_recess_d + wall + cavity_d/2, case_h/2])
cube([case_w - 2*wall, cavity_d, case_h - 2*wall], center=true);
// Rear-wall pass-through slots for reusable cloth zip-ties / Velcro.
// The slots align with the raised bridges, so straps can thread through
// the back of the case instead of sitting in a closed decorative groove.
for (zc=[strap_z1, strap_z2])
translate([0, case_d/2 + 1.0, zc])
cube([strap_bridge_w - 8.0, 8.0, strap_slot_h], center=true);
// Interior electronics cavity: ESP32-C3 Super Mini + ESP-01S plus wiring/service clearance.
cavity_d = case_d - front_recess_d - 2*wall;
translate([0, -case_d/2 + front_recess_d + wall + cavity_d/2, case_h/2])
cube([case_w - 2*wall, cavity_d, case_h - 2*wall], center=true);
// Bottom cable exit / strain-relief notch on lower front edge.
translate([0, -case_d/2 + 2.0, -0.05])
cube([cable_notch_w, 9.0, cable_notch_h], center=true);
}
// Bottom center cable exit / strain-relief notch.
translate([0, -case_d/2 + 2.0, -0.05])
xz_rounded_prism(cable_notch_w, 9.0, cable_notch_h, r=1.8);
}
// Four protected screw bosses are added after shell hollowing so the
// electronics cavity cannot cut away the receiving material.
screw_x = lid_w/2 - 5.0;
screw_z_low = (case_h - lid_h)/2 + 4.2;
screw_z_low = (case_h - lid_h)/2 + 5.0;
screw_z_high = case_h - screw_z_low;
screw_boss(-screw_x, screw_z_low);
screw_boss( screw_x, screw_z_low);
@@ -154,11 +154,9 @@ module camera_node_body_v4() {
screw_boss( screw_x, screw_z_high);
}
// Final body-level pilot holes cut through the front frame into the
// protected bosses. Without this pass the boss pilot holes can be
// hidden behind solid frame material.
// Final body-level pilot holes cut through the front frame into the protected bosses.
screw_x = lid_w/2 - 5.0;
screw_z_low = (case_h - lid_h)/2 + 4.2;
screw_z_low = (case_h - lid_h)/2 + 5.0;
screw_z_high = case_h - screw_z_low;
for (x=[-screw_x, screw_x], z=[screw_z_low, screw_z_high])
translate([x, -case_d/2 + front_recess_d + boss_len/2, z])
@@ -170,45 +168,41 @@ module camera_node_body_v4() {
module camera_node_lid_v4() {
difference() {
union() {
// Visible flush panel.
translate([0,0,0]) rounded_box([lid_w, lid_t, lid_h], r=0.65);
// Visible full-height flush panel; restrained and not a busy slab.
rounded_box([lid_w, lid_t, lid_h], r=0.65);
// Rear locating lip fits inside the central service opening and helps
// panel alignment. Center it on the body's central opening so it
// does not collide with lower vent geometry.
lip_h = service_opening_h - 0.5;
translate([0, lid_t/2 + lid_lip_t/2 - 0.2, (lid_h - lip_h)/2])
rounded_box([service_opening_w - 0.5, lid_lip_t, lip_h], r=0.45);
// Shallow raised text pads/labels are omitted for print simplicity; add decals or paint-fill.
// Rear locating lip fits inside the large service opening.
translate([0, lid_t/2 + lid_lip_t/2 - 0.2, lid_h/2])
xz_rounded_prism(service_opening_w - 0.8, lid_lip_t, service_opening_h - 0.8, r=1.5);
}
// OLED window and shallow module outline/recess.
translate([0, -0.2, lid_h - 9.5])
cube([oled_window_w, lid_t + lid_lip_t + 0.8, oled_window_h], center=true);
translate([0, -0.35, lid_h - 9.5])
cube([oled_bezel_w, 0.7, oled_bezel_h], center=true);
// OLED window and shallow black-bezel-style recess near the top.
translate([0, -lid_t/2 + 0.35, oled_z])
xz_rounded_prism(oled_bezel_w, 0.9, oled_bezel_h, r=1.3);
translate([0, -0.2, oled_z])
xz_rounded_prism(oled_window_w, lid_t + lid_lip_t + 0.9, oled_window_h, r=0.5);
// LED holes: separate power LED and single RGB status LED.
translate([-17.0, -0.2, 11.5]) y_cylinder(d=led_hole_d, h=lid_t + lid_lip_t + 1.0);
translate([-9.0, -0.2, 11.5]) y_cylinder(d=led_hole_d, h=lid_t + lid_lip_t + 1.0);
// Subtle secondary indicators flanking the rocker, below the OLED bezel.
translate([-15.0, -0.2, 33.0]) y_cylinder(d=led_hole_d, h=lid_t + lid_lip_t + 1.0);
translate([ 15.0, -0.2, 33.0]) y_cylinder(d=led_hole_d, h=lid_t + lid_lip_t + 1.0);
// Rocker switch snap-in opening.
translate([15.5, -0.2, 12.0])
cube([rocker_w, lid_t + lid_lip_t + 1.0, rocker_h], center=true);
// Small rocker lower on the panel, offset away from the OLED, screws, and slots.
translate([0, -0.2, 33.0])
xz_rounded_prism(rocker_w, lid_t + lid_lip_t + 1.0, rocker_h, r=0.8);
// Lower vent / style slots.
for (i=[-2:2])
translate([i*9.5, -0.2, 4.8])
cube([vent_slot_w, lid_t + lid_lip_t + 1.0, vent_slot_h], center=true);
// Two long rounded horizontal slots near the lower front, matching the reference.
translate([0, -0.2, 17.0])
xz_rounded_prism(front_slot_w, lid_t + lid_lip_t + 1.0, front_slot_h, r=front_slot_h/2);
translate([0, -0.2, 11.0])
xz_rounded_prism(front_slot_w, lid_t + lid_lip_t + 1.0, front_slot_h, r=front_slot_h/2);
// Screw clearance/counterbore holes.
screw_x = lid_w/2 - 5.0;
screw_z_low = 4.2;
screw_z_low = 5.0;
screw_z_high = lid_h - screw_z_low;
for (x=[-screw_x, screw_x], z=[screw_z_low, screw_z_high]) {
translate([x, -0.2, z]) y_cylinder(d=screw_d + 0.4, h=lid_t + lid_lip_t + 1.0);
translate([x, -0.15, z]) y_cylinder(d=screw_head_d, h=1.2);
translate([x, -lid_t/2 + 0.55, z]) y_cylinder(d=screw_head_d, h=1.3);
}
}
}
@@ -217,12 +211,17 @@ module camera_node_lid_v4() {
module camera_node_preview_v4(show_lid=true) {
color("lightgray") camera_node_body_v4();
if (show_lid)
translate([0, -case_d/2 - lid_t/2 - 0.2, (case_h - lid_h)/2])
translate([0, -case_d/2 + lid_t/2 + 0.03, (case_h - lid_h)/2])
color("gainsboro") camera_node_lid_v4();
// Dark OLED bezel/window cue for visual review only (not part of exported lid STL when rendering lid).
if (show_lid)
translate([0, -case_d/2 - 0.08, (case_h - lid_h)/2 + oled_z])
color("black") xz_rounded_prism(oled_bezel_w, 0.6, oled_bezel_h, r=1.3);
// Internal board volume guides (not printed): ESP32-C3 and ESP-01S envelopes.
color([0,0.45,0,0.35]) translate([-13, -1, 9]) cube([22.5, 18, 4], center=true);
color([0,0.2,0.8,0.35]) translate([13, -1, 20]) cube([24.7, 14.3, 12], center=true);
color([0,0.45,0,0.35]) translate([-9, -1, 26]) cube([22.5, 18, 4], center=true);
color([0,0.2,0.8,0.35]) translate([9, -1, 45]) cube([24.7, 14.3, 12], center=true);
}
// Select part to render from OpenSCAD CLI with: -D 'part="body"'