// Hermes robot enclosure — Raspberry Pi 4 + Adafruit BrainCraft HAT // // Layout: an upright cabinet holding the Pi/HAT stack portrait-wise, the display as the face, // a speaker behind a grille below it, all turning on a bearing turntable (pan). A small yoke on // top tilts only the camera. // // Coordinates (body): X = width, viewer's left to right; Y = depth, front face at Y=0 going back; // Z = height, up. Pi coordinates: px along the board's length from the microSD end (0..85), // py across it from the USB-C edge (0..56). The stack stands with px pointing down, py to the right. // // EVERY value marked MEASURE was estimated from photos or datasheets. Check it with calipers and // print the test pieces (PART = "test_*") before printing the big parts. // // Export one part: openscad -D 'PART="body"' -o body.stl hermes_robot.scad /* [Part] */ PART = "assembly"; // [assembly, exploded, body, back, plate, base, yoke, cradle, test_front, test_bearing, test_servo, test_yoke] $fn = 72; eps = 0.01; wall = 2.4; // walls and front face tol = 0.3; // general sliding clearance // ============================================================ MEASURE: Pi 4 + BrainCraft stack pi_len = 85; pi_wid = 56; pcb = 1.6; pi_holes = [[3.5,3.5],[61.5,3.5],[3.5,52.5],[61.5,52.5]]; // [px, py], same on Pi and HAT port_over = 2.5; // USB/Ethernet past the board end stack_gap = 11; // Pi top surface to HAT underside (standoff length) MEASURE hat_clear = 6.5; // tallest part on the HAT front, display excluded MEASURE pi_back_clear = 3; // solder pins / screw heads under the Pi MEASURE head_d = 5.6; head_h = 2.2; // M2.5 screw heads holding the stack MEASURE // HAT front features, in Pi coordinates [px, py] MEASURE ALL glass_px = [6.9, 40.7]; glass_py = [10.9, 44.7]; // display glass extents win_w = 28.6; win_h = 28.6; win_r = 1.5; // visible window (active area 27.7 sq) win_shift = [0, 0]; // nudge window [dpx, dpy] mic_l = [4.9, 21.7]; mic_r = [62.2, 20.2]; sw_c = [5.5, 40.0]; sw_len = 9; sw_wid = 4.5; // audio switch slot (length along py) leds = [[44.6,41.8],[53.4,41.8],[61.8,41.8]]; soc = [29, 32]; // Pi SoC, for the fan sd_py = 28; // microSD centre // ============================================================ MEASURE: speaker (Adafruit 5986 box) spk = [70, 17.2, 30.2]; // X width, Y depth, Z height // ============================================================ MEASURE: MG90S servo + double-arm horn srv_l = 22.8; srv_w = 12.2; // case length (along tabs) and width srv_tab_z = 16.0; srv_tab_t = 2.5; srv_tab_len = 32.2; srv_hole_sp = 27.6; srv_shaft_off = 5.9; // shaft centre from the near end of the case srv_tip_h = 32.0; // case bottom to spline tip srv_bump_h = 26.0; // case bottom to top of the gear bump horn_len = 16; // shaft centre to arm tip horn_hub_w = 7.5; horn_tip_w = 4.2; horn_t = 1.8; // arm widths and thickness horn_seat = 0.5; // spline tip sits this far below the horn's top face horn_holes = [9, 13]; // radii of the screw holes along each arm // ============================================================ MEASURE: bearing 6810-2RS (50 x 65 x 7) brg_id = 50; brg_od = 65; brg_h = 7; brg_in_shoulder = 55; // inner ring outer diameter (plate shoulder must stay inside) brg_out_shoulder = 59.5; // outer ring inner diameter press = 0.15; // press-fit interference; tune with test_bearing // ============================================================ MEASURE: camera (Arducam UC-346 / Pi cam v1 size) cam = [25, 24]; cam_pcb = 1.0; cam_holes = [[2,2],[23,2],[2,14.5],[23,14.5]]; // from top-left, x right, y down cam_lens_top = 9.46; // lens centre below the board's top edge cam_conn_h = 6; // ribbon connector band at the bottom of the board's back cam_standoff = 3; // board back to cradle plate // ============================================================ layout choices W = 86; // body width top_clear = 11; // above the board end (JST overhang + plug) plenum = 8; // cable space under the speaker shelf_t = 2; rear_air = 11; // behind the Pi: airflow and a 30 x 30 x 10 mm fan R_out = 7; // corner radius seen from the front label = "HERMES"; // engraved under the grille; "" for none fan = true; // 30 mm fan mount in the back cover fan_sz = 30; fan_hole_sp = 24; plate_d = 76; plate_t = 4; gap_turn = 0.8; tilt_axis_h = 26; // tilt axis above the body top (horn flange must clear the yoke base) cam_x_offset = 0; // shift camera + yoke sideways if the servo overhang bothers you // ============================================================ derived disp_px_c = (glass_px[0]+glass_px[1])/2 + win_shift[0]; disp_py_c = (glass_py[0]+glass_py[1])/2 + win_shift[1]; x0 = W/2 - disp_py_c; // body X of the board's py=0 edge z_spk0 = wall + plenum; z_shelf = z_spk0 + spk[2] + 0.6; z_pi_end = z_shelf + shelf_t + 2; z_top_board = z_pi_end + port_over + pi_len; // body Z of px=0 H = z_top_board + top_clear + wall; y_in = wall; y_hat_f = y_in + 0.5 + hat_clear; y_hat_b = y_hat_f + pcb; y_pi_t = y_hat_b + stack_gap; y_pi_b = y_pi_t + pcb; y_back_in = y_pi_b + pi_back_clear + rear_air; D = y_back_in + wall; axis = [W/2, D/2]; // pan axis in body XY spk_x1 = W - wall - 0.6; spk_x0 = spk_x1 - spk[0]; spk_y0 = y_in + 0.5; function BX(py) = x0 + py; function BZ(px) = z_top_board - px; echo(str("BODY W=", W, " D=", D, " H=", H)); echo(str("left clearance beside board = ", x0 - wall, " mm (USB-C plug side)")); echo(str("speaker cable channel = ", spk_x0 - wall, " mm")); assert(x0 - wall >= 11, "not enough room for a right-angle USB-C plug on the left"); // servo geometry along its own axis (0 = case bottom, + toward the spline tip) srv_case_c = srv_l/2 - srv_shaft_off; // case centre relative to shaft, along its length // base stack: everything follows from where the spline tip must be base_floor = 2.4; z_srv_bottom = base_floor + 1.5; base_h = z_srv_bottom + srv_tip_h - gap_turn - horn_t + horn_seat; // bearing top flush with base top z_brg0 = base_h - brg_h; echo(str("BASE height=", base_h)); // tilt yoke geometry (local: origin on tilt axis at cradle centre, X along axis, camera looks -Y) cb_r = 17; // right boss outer face cb_l = 13.6; // left flange inner face (just clear of the camera board) fl_t = horn_t + 1.6; // left flange thickness x_tip = -cb_l - fl_t + horn_t - horn_seat; // tilt servo spline tip x_tabtop = x_tip - (srv_tip_h - srv_tab_z - srv_tab_t); x_srv_bottom = x_tip - srv_tip_h; y_tilt = 10.5; // tilt axis in body Y mp_t = 6; // servo mount plate thickness yb_t = 4; // yoke base plate thickness yoke_y = [-10.5, 22.5]; yoke_bolts = [[-15, 20], [17, 20]]; // local [X, Y] echo(str("tilt servo bottom overhangs body side by ", -(W/2 + cam_x_offset + x_srv_bottom), " mm")); // ============================================================ helpers module rrect(w, h, r) { r2 = max(0.01, min(r, w/2 - 0.01, h/2 - 0.01)); hull() for (x = [r2, w - r2], y = [r2, h - r2]) translate([x, y]) circle(r = r2); } // rounded rectangle in the XZ plane at corner (x, z), spanning y0..y1 module xz_rr(x, z, w, h, r, y0, y1) { translate([x, y1, z]) rotate([90, 0, 0]) linear_extrude(y1 - y0) rrect(w, h, r); } module xz_rr_c(cx, cz, w, h, r, y0, y1) xz_rr(cx - w/2, cz - h/2, w, h, r, y0, y1); // cylinder along +Y module ycyl(x, y0, z, d, len) translate([x, y0, z]) rotate([-90, 0, 0]) cylinder(d = d, h = len); // through the front wall, with a 45 degree chamfer on the outside module front_cut(cx, cz, w, h, r, ch) { xz_rr_c(cx, cz, w, h, r, -1, wall + 1); if (ch > 0) hull() { xz_rr_c(cx, cz, w + 2*ch, h + 2*ch, r + ch, -1, 0); xz_rr_c(cx, cz, w, h, r, ch, ch + eps); } } // ============================================================ BODY SHELL (print front face down) module boss_corner(xc, zc, side) { // screw boss in a corner, 45 degree underside for printing bs = 7; L = 10; x = side < 0 ? wall : W - wall - bs; wx = side < 0 ? wall : W - wall - eps; hull() { translate([x, y_back_in - L, zc - bs/2]) cube([bs, L, bs]); translate([wx, y_back_in - L - bs, zc - bs/2]) cube([eps, eps, bs]); } } boss_z = [H - wall - 3.5, z_shelf + shelf_t + 3.5]; function boss_pts() = [for (z = boss_z, s = [-1, 1]) [s < 0 ? wall + 3.5 : W - wall - 3.5, z, s]]; plate_screws = [[-31,-10],[31,-10],[-31,10],[31,10]]; // relative to the pan axis module body() { difference() { union() { difference() { xz_rr(0, 0, W, H, R_out, 0, D); xz_rr(wall, wall, W - 2*wall, H - 2*wall, R_out - wall, y_in, D + 1); } // shelf over the speaker; the left strip stays open as the cable channel translate([spk_x0, y_in - eps, z_shelf]) cube([W - wall - spk_x0 + eps, y_back_in - y_in, shelf_t]); // speaker feet for (x = [spk_x0 + 12, spk_x1 - 17], y = [spk_y0 + 1, spk_y0 + spk[1] - 6]) translate([x, y, wall - eps]) cube([5, 5, plenum + eps]); // locating cups for the HAT's front screw heads for (h = pi_holes) difference() { ycyl(BX(h[1]), y_in - 0.5, BZ(h[0]), 7, (y_hat_f - 0.4) - (y_in - 0.5)); ycyl(BX(h[1]), y_hat_f - head_h, BZ(h[0]), head_d + 0.4, 5); } for (b = boss_pts()) boss_corner(b[0], b[1], b[2]); // ledge that stops the back cover at the floor translate([wall + R_out, y_back_in - 2.5, wall - eps]) cube([W - 2*wall - 2*R_out, 2.5, 2.5]); } // ---- front face cx = BX(disp_py_c); cz = BZ(disp_px_c); front_cut(cx, cz, win_w, win_h, win_r, 1.8); // deep chamfer: a recessed "tube face" for (m = [mic_l, mic_r]) { ycyl(BX(m[1]), -1, BZ(m[0]), 2.0, wall + 2); translate([BX(m[1]), -eps, BZ(m[0])]) rotate([-90, 0, 0]) cylinder(d1 = 3.6, d2 = 2.0, h = 0.8); } front_cut(BX(sw_c[1]), BZ(sw_c[0]), sw_len, sw_wid, 1.2, 0.6); for (l = leds) ycyl(BX(l[1]), 0.6, BZ(l[0]), 5, wall); // thin skin: LEDs glow through PETG // speaker grille gcx = (spk_x0 + spk_x1)/2; gcz = z_spk0 + spk[2]/2; for (i = [-2.5 : 1 : 2.5]) front_cut(gcx, gcz + i*4.2, 54, 2.2, 1.1, 0.6); if (label != "") translate([gcx, 0.6, wall + plenum/2 + 0.3]) rotate([90, 0, 0]) linear_extrude(2) text(label, size = 4.2, halign = "center", valign = "center", font = "Liberation Sans:style=Bold", spacing = 1.25); // ---- top tx = axis[0] + cam_x_offset; translate([tx - 9.5, y_tilt + 0.5, H - wall - 1]) cube([19, 3.5, wall + 2]); // camera ribbon, drops into the Pi-HAT gap translate([max(R_out + 5, tx + x_srv_bottom + 7), y_tilt + 18, H - wall - 1]) cylinder(d = 7, h = wall + 2); // tilt servo lead for (b = yoke_bolts) translate([tx + b[0], y_tilt + b[1], H - wall - 1]) cylinder(d = 3.4, h = wall + 2); translate([BX(sd_py) - 8, y_pi_b - 1, H - wall - 1]) cube([16, 5, wall + 2]); // microSD access // ---- sides: vents high and low for (s = [0, 1], zc = [z_top_board - 14, z_pi_end + 14], i = [0 : 3]) translate([s ? W - wall - 1 : -1, y_hat_b + 1 + i*5.5, zc - 9]) cube([wall + 2, 2.2, 18]); // ---- floor: cable hole and turntable plate screws translate([axis[0], axis[1], -1]) cylinder(d = 26, h = wall + 2); for (p = plate_screws) translate([axis[0] + p[0], axis[1] + p[1], -1]) cylinder(d = 3.4, h = wall + 2); // ---- back screw pilots for (b = boss_pts()) translate([b[0], y_back_in - 9, b[1]]) rotate([-90, 0, 0]) cylinder(d = 2.6, h = 10); } } // ============================================================ BACK COVER (print flat, outside down) module back() { cw = W - 2*wall - 2*tol; ch = H - 2*wall - 2*tol; difference() { union() { xz_rr(wall + tol, wall + tol, cw, ch, R_out - wall - tol, y_back_in, D); // clamp the stack: cups press the Pi's rear screw heads for (h = pi_holes) difference() { ycyl(BX(h[1]), y_pi_b + 0.4, BZ(h[0]), 7, y_back_in - (y_pi_b + 0.4) + eps); ycyl(BX(h[1]), y_pi_b - 1, BZ(h[0]), head_d + 0.4, head_h + 1); } // two bars hold the speaker against the front wall for (x = [spk_x0 + 8, spk_x1 - 14]) translate([x, spk_y0 + spk[1] + 0.2, z_spk0 + 6]) cube([6, y_back_in - (spk_y0 + spk[1] + 0.2) + eps, spk[2] - 12]); } for (b = boss_pts()) { // countersunk M3 translate([b[0], y_back_in - 1, b[1]]) rotate([-90, 0, 0]) cylinder(d = 3.4, h = wall + 2); translate([b[0], D - 1.8 + eps, b[1]]) rotate([-90, 0, 0]) cylinder(d1 = 3.4, d2 = 6.6, h = 1.8); } fx = BX(soc[1]); fz = BZ(soc[0]); if (fan) { ycyl(fx, y_back_in - 1, fz, fan_sz - 2, wall + 2); for (i = [-1, 1], j = [-1, 1]) ycyl(fx + i*fan_hole_sp/2, y_back_in - 1, fz + j*fan_hole_sp/2, 3.0, wall + 2); } // intake slots low over the Pi's port end and the speaker shelf; exhaust high for (zc = [z_pi_end + 10, z_top_board - 6], i = [-3 : 3]) if (!fan || abs(zc - fz) > fan_sz/2 + 4) xz_rr_c(W/2 + i*7.5, zc, 2.6, 14, 1.3, y_back_in - 1, D + 1); } } // ============================================================ TURNTABLE PLATE (print top face down) // local: pan axis at origin, z = 0 is the plate's top (screwed under the body floor) module horn_2d(extra = 0) { hull() { circle(d = horn_hub_w + 2*extra); translate([0, horn_len]) circle(d = horn_tip_w + 2*extra); } hull() { circle(d = horn_hub_w + 2*extra); translate([0, -horn_len]) circle(d = horn_tip_w + 2*extra); } } module plate() { hub_od = brg_id - press; difference() { union() { difference() { translate([0, 0, -plate_t]) cylinder(d = plate_d, h = plate_t); translate([0, 0, -plate_t - 1]) cylinder(d = hub_od - 6, h = plate_t + 2); } // bridge across the bore along Y carries the servo horn intersection() { translate([0, 0, -plate_t]) cylinder(d = hub_od - 1, h = plate_t); translate([0, 0, -plate_t]) linear_extrude(plate_t) union() { offset(r = 2.5) horn_2d(); square([horn_tip_w + 5, brg_id], center = true); // spokes reach the ring } } translate([0, 0, -plate_t - gap_turn]) difference() { // shoulder on the inner ring cylinder(d = brg_in_shoulder - 1, h = gap_turn + eps); translate([0, 0, -1]) cylinder(d = hub_od - 6, h = gap_turn + 2); } translate([0, 0, -plate_t - gap_turn - brg_h]) difference() { // hub pressed into the bore cylinder(d = hub_od, h = brg_h + eps); translate([0, 0, -1]) cylinder(d = hub_od - 6, h = brg_h + 2); cylinder(d1 = hub_od - 1.2, d2 = hub_od - 6, h = 1.2); // lead-in } } // horn pocket and screw holes translate([0, 0, -plate_t - eps]) linear_extrude(horn_t + 0.2) offset(r = 0.2) horn_2d(); translate([0, 0, -plate_t - 1]) cylinder(d = 6, h = plate_t + 2); // horn centre screw for (r = horn_holes, s = [-1, 1]) translate([0, s*r, -plate_t - 1]) cylinder(d = 1.5, h = plate_t + 2); // blind pilots for the body screws (driven from inside the body) for (p = plate_screws) translate([p[0], p[1], -plate_t + 0.6]) cylinder(d = 2.6, h = plate_t); } } // ============================================================ BASE (print upright) // local: pan axis at origin, z = 0 on the table module base() { r_top = brg_od/2 + 5; r_bot = 45; difference() { union() { cylinder(r1 = r_bot, r2 = r_bot, h = 3); translate([0, 0, 3]) cylinder(r1 = r_bot, r2 = r_top, h = 9); translate([0, 0, 12 - eps]) cylinder(r = r_top, h = base_h - 12 + eps); } translate([0, 0, base_floor]) cylinder(r = brg_out_shoulder/2 + 0.3, h = base_h); // cavity translate([0, 0, z_brg0]) cylinder(d = brg_od + 0.1, h = brg_h + 1); // bearing seat translate([0, 0, base_h - 0.6]) cylinder(d1 = brg_od + 0.1, d2 = brg_od + 1.3, h = 0.61); // lead-in translate([-8, 25, base_floor]) cube([16, 30, 10]); // cable exit, back for (a = [45 : 90 : 315]) rotate(a) translate([37, 0, -eps]) cylinder(d = 10.5, h = 1.2); // feet } // servo posts under the tabs (servo length along X, shaft on the axis) for (hx = [srv_case_c - srv_hole_sp/2, srv_case_c + srv_hole_sp/2]) { pw = (srv_tab_len - srv_l)/2 - 0.6; px = hx < srv_case_c ? srv_case_c - srv_l/2 - 0.3 - pw : srv_case_c + srv_l/2 + 0.3; difference() { translate([px, -5, base_floor - eps]) cube([pw, 10, z_srv_bottom + srv_tab_z - base_floor + eps]); translate([hx, 0, z_srv_bottom + srv_tab_z - 8]) cylinder(d = 1.8, h = 9); } } } // ============================================================ TILT YOKE (print upright) // local: tilt axis = X axis, origin at cradle centre; base plate underside at z = -tilt_axis_h module yoke() { zb = -tilt_axis_h; difference() { union() { // base plate translate([x_tabtop, yoke_y[0], zb]) cube([cb_r + 7 - x_tabtop, yoke_y[1] - yoke_y[0], yb_t]); // servo mount plate: tabs bear on its outer (-X) face translate([x_tabtop, srv_case_c - srv_tab_len/2 - 2.5, zb]) cube([mp_t, srv_tab_len + 5, tilt_axis_h + srv_w/2 + 3]); // right upright carrying the M3 axle hull() { translate([cb_r + 1, -7, zb]) cube([6, 14, yb_t]); translate([cb_r + 1, 0, 0]) rotate([0, 90, 0]) cylinder(d = 12, h = 6); } } // servo case through the mount plate translate([x_tabtop - 1, srv_case_c - srv_l/2 - 0.3, -srv_w/2 - 0.3]) cube([mp_t + 2, srv_l + 0.6, srv_w + 0.6]); for (s = [-1, 1]) translate([x_tabtop - 1, srv_case_c + s*srv_hole_sp/2, 0]) rotate([0, 90, 0]) cylinder(d = 1.8, h = mp_t + 2); // axle translate([cb_r - 1, 0, 0]) rotate([0, 90, 0]) cylinder(d = 3.3, h = 10); // ribbon slot, microSD access, bolts translate([-9.5, 0.5, zb - 1]) cube([19, 3.5, yb_t + 2]); translate([BX(sd_py) - axis[0] - cam_x_offset - 8, y_pi_b - 1 - y_tilt, zb - 1]) cube([16, 5, yb_t + 2]); for (b = yoke_bolts) translate([b[0], b[1], zb - 1]) cylinder(d = 3.4, h = yb_t + 2); // lighten translate([-4, 6, zb - 1]) cube([12, 10, yb_t + 2]); } } // ============================================================ CAMERA CRADLE (print flat, plate down) module cradle() { pz0 = -cam_lens_top - (cam[1] - cam_lens_top) + cam_conn_h + 0.5; // plate bottom (clears connector) pz1 = cam_lens_top + 2; py0 = cam_standoff - 1.5; pt = 3; // plate in Y difference() { union() { translate([-cam[0]/2 - 2, py0, pz0]) cube([cam[0] + 4, pt, pz1 - pz0]); for (h = cam_holes) translate([h[0] - cam[0]/2, py0 + eps, cam_lens_top - h[1]]) rotate([90, 0, 0]) cylinder(d = 4.6, h = cam_standoff); // right boss to the axle hull() { translate([cam[0]/2 + 2 - eps, py0, -4.5]) cube([eps, pt, 9]); translate([cb_r - 4, 0, 0]) rotate([0, 90, 0]) cylinder(d = 9, h = 4); } // left: boss to the horn flange hull() { translate([-cam[0]/2 - 2, py0, -4.5]) cube([eps, pt, 9]); translate([-cb_l, 0, 0]) rotate([0, 90, 0]) cylinder(d = 9, h = 1); } translate([-cb_l - fl_t, 0, 0]) rotate([0, 90, 0]) linear_extrude(fl_t) rotate(90) offset(r = 2.5) horn_2d(); } for (h = cam_holes) translate([h[0] - cam[0]/2, py0 + pt + 1, cam_lens_top - h[1]]) rotate([90, 0, 0]) cylinder(d = 1.7, h = pt + cam_standoff + 2); translate([-cb_l - fl_t - eps, 0, 0]) rotate([0, 90, 0]) linear_extrude(horn_t + 0.2) rotate(90) offset(r = 0.2) horn_2d(); translate([-cb_l - fl_t - 1, 0, 0]) rotate([0, 90, 0]) cylinder(d = 6, h = fl_t + 2); for (r = horn_holes, s = [-1, 1]) translate([-cb_l - fl_t - 1, 0, s*r]) rotate([0, 90, 0]) cylinder(d = 1.5, h = fl_t + 2); translate([cb_r + eps, 0, 0]) rotate([0, -90, 0]) cylinder(d = 2.6, h = 9); // M3 axle pilot } } // ============================================================ dummy hardware for the assembly view module dummy_stack() { color("darkgreen") translate([BX(0), y_pi_t, BZ(pi_len)]) cube([pi_wid, pcb, pi_len]); color("silver") translate([BX(10), y_pi_t - 16, BZ(pi_len + port_over)]) cube([36, 16, 20]); color("black") translate([BX(0), y_hat_f, BZ(65)]) cube([pi_wid, pcb, 65]); color("dimgray") translate([BX(glass_py[0]), y_hat_f - 4, BZ(glass_px[1])]) cube([glass_py[1]-glass_py[0], 4, glass_px[1]-glass_px[0]]); color("gold") for (h = pi_holes) ycyl(BX(h[1]), y_hat_f - head_h, BZ(h[0]), head_d, head_h); } module dummy_speaker() color("dimgray") translate([spk_x0, spk_y0, z_spk0]) cube(spk); module dummy_servo() color("royalblue") { // standing: shaft up at origin translate([srv_case_c - srv_l/2, -srv_w/2, -srv_tip_h]) cube([srv_l, srv_w, srv_bump_h - 3]); translate([srv_case_c - srv_tab_len/2, -srv_w/2, -srv_tip_h + srv_tab_z]) cube([srv_tab_len, srv_w, srv_tab_t]); translate([0, 0, -srv_tip_h]) cylinder(d = 11.5, h = srv_bump_h); translate([0, 0, -srv_tip_h]) cylinder(d = 4.8, h = srv_tip_h); } module dummy_bearing() color("lightsteelblue") difference() { cylinder(d = brg_od, h = brg_h); translate([0,0,-1]) cylinder(d = brg_id, h = brg_h + 2); } module dummy_camera() color("forestgreen") { translate([-cam[0]/2, -cam_standoff + 1.5 - cam_pcb, cam_lens_top - cam[1]]) cube([cam[0], cam_pcb, cam[1]]); translate([-4, -cam_standoff + 1.5 - cam_pcb - 5, -4]) cube([8, 5, 8]); } // ============================================================ placement z_body0 = base_h + gap_turn + plate_t; // body bottom above the table module placed(exploded = 0) { e = exploded; translate([-axis[0], -axis[1], 0]) { translate([axis[0], axis[1], 0]) base(); translate([axis[0], axis[1], z_brg0]) dummy_bearing(); translate([axis[0], axis[1], base_h + gap_turn + horn_t - horn_seat]) dummy_servo(); translate([axis[0], axis[1], z_body0 + e*0.6]) plate(); translate([0, 0, z_body0 + e]) { color("tan", 0.55) body(); color("burlywood") translate([0, e*1.2, 0]) back(); dummy_stack(); dummy_speaker(); translate([axis[0] + cam_x_offset, y_tilt, H + tilt_axis_h + e*0.6]) { color("peru") yoke(); color("sandybrown") translate([0, 0, e*0.3]) cradle(); translate([0, 0, e*0.3]) dummy_camera(); translate([x_tip, 0, 0]) rotate([0, 90, 0]) rotate([0, 0, 90]) dummy_servo(); } } } } // ============================================================ beauty render (opaque, display keyed in magenta) module beauty() { translate([-axis[0], -axis[1], 0]) { color([0.20, 0.21, 0.23]) translate([axis[0], axis[1], 0]) base(); color([0.30, 0.31, 0.33]) translate([axis[0], axis[1], z_body0]) plate(); translate([0, 0, z_body0]) { color([0.50, 0.50, 0.49]) body(); color([1, 0, 1]) translate([BX(glass_py[0]) - 3, y_in + 0.6, BZ(glass_px[1]) - 3]) cube([glass_py[1] - glass_py[0] + 6, 0.5, glass_px[1] - glass_px[0] + 6]); color([0.05, 0.05, 0.06]) translate([wall + 1, y_in + 2, wall + 1]) cube([W - 2*wall - 2, 2, H - 2*wall - 2]); // dark interior behind slots translate([axis[0] + cam_x_offset, y_tilt, H + tilt_axis_h]) rotate([8, 0, 0]) { color([0.36, 0.33, 0.30]) rotate([-8, 0, 0]) yoke(); color([0.33, 0.31, 0.29]) cradle(); color([0.12, 0.18, 0.14]) translate([-cam[0]/2, -cam_standoff + 1.5 - cam_pcb, cam_lens_top - cam[1]]) cube([cam[0], cam_pcb, cam[1]]); color([0.05, 0.05, 0.05]) translate([-4, -cam_standoff + 1.5 - cam_pcb - 5, -4]) cube([8, 5, 8]); color([0.15, 0.3, 0.35]) translate([0, -cam_standoff + 1.5 - cam_pcb - 5.2, 0]) rotate([90, 0, 0]) cylinder(d = 5, h = 0.4); color([0.15, 0.17, 0.22]) rotate([-8, 0, 0]) translate([x_tip, 0, 0]) rotate([0, 90, 0]) rotate([0, 0, 90]) dummy_servo(); } } } } // ============================================================ posed parts for external rendering pose_pan = -12; // body turned toward a viewer at front-left pose_tilt = 8; // camera dipped slightly module pose(n) { if (n == "base") base(); if (n == "servo_pan") translate([0, 0, base_h + gap_turn + horn_t - horn_seat]) dummy_servo(); rotate([0, 0, pose_pan]) { if (n == "plate") translate([0, 0, z_body0]) plate(); translate([-axis[0], -axis[1], z_body0]) { if (n == "body") body(); translate([axis[0] + cam_x_offset, y_tilt, H + tilt_axis_h]) { if (n == "yoke") yoke(); if (n == "servo_tilt") translate([x_tip, 0, 0]) rotate([0, 90, 0]) rotate([0, 0, 90]) dummy_servo(); rotate([pose_tilt, 0, 0]) { if (n == "cradle") cradle(); if (n == "camera") dummy_camera(); } } } } } // window and LED positions in the same posed frame, for compositing function posed(p) = let(a = pose_pan, q = [p[0] - axis[0], p[1] - axis[1], p[2] + z_body0]) [q[0]*cos(a) - q[1]*sin(a), q[0]*sin(a) + q[1]*cos(a), q[2]]; if (PART == "pose_info") { cx = BX(disp_py_c); cz = BZ(disp_px_c); for (c = [[-1, 1], [1, 1], [1, -1], [-1, -1]]) echo(str("WIN ", posed([cx + c[0]*win_w/2, wall * 0.6, cz + c[1]*win_h/2]))); for (l = leds) echo(str("LED ", posed([BX(l[1]), 0.6, BZ(l[0])]))); echo(str("NORMAL ", posed([0, -1, 0]) - posed([0, 0, 0]))); } if (len(PART) > 5 && PART[0] == "p" && PART[1] == "o" && PART[2] == "s" && PART[3] == "e" && PART[4] == "_" && PART != "pose_info") pose(chr([for (i = [5 : len(PART) - 1]) ord(PART[i])])); // ============================================================ test pieces module test_front() intersection() { // front wall over the HAT, with the locating cups body(); translate([BX(-3), -1, BZ(68)]) cube([pi_wid + 6, y_hat_f + 1, 72]); } module test_bearing() { intersection() { base(); translate([0, 0, z_brg0 - 2]) cylinder(r = 50, h = 20); } translate([80, 0, 0]) rotate([180, 0, 0]) intersection() { // hub ring, plate face down as printed plate(); translate([0, 0, -30]) cylinder(d = brg_in_shoulder + 2, h = 30); } } module test_servo() intersection() { base(); cylinder(r = 26, h = z_srv_bottom + srv_tab_z + 1); } module test_yoke() intersection() { yoke(); translate([x_tabtop - 1, -40, -tilt_axis_h - 1]) cube([mp_t + 2, 80, 80]); } // ============================================================ output if (PART == "assembly") placed(0); else if (PART == "exploded") placed(28); else if (PART == "beauty") beauty(); else if (PART == "body") rotate([90, 0, 0]) body(); // front face on the bed else if (PART == "back") translate([0, 0, D]) rotate([-90, 0, 0]) back(); // outside face on the bed else if (PART == "plate") rotate([180, 0, 0]) plate(); // top face on the bed else if (PART == "base") base(); else if (PART == "yoke") translate([0, 0, tilt_axis_h]) yoke(); else if (PART == "cradle") translate([0, 0, cb_l + fl_t]) rotate([0, -90, 0]) cradle(); // horn flange on the bed else if (PART == "test_front") rotate([90, 0, 0]) test_front(); else if (PART == "test_bearing") test_bearing(); else if (PART == "test_servo") test_servo(); else if (PART == "test_yoke") translate([0, 0, tilt_axis_h]) test_yoke();