513 lines
27 KiB
OpenSCAD
513 lines
27 KiB
OpenSCAD
// 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();
|