// MB-001 — REMOTE pan-tilt stereo camera rig (concept CAD, parametric) // Rig only: base (1/4"-20 tripod hole / desk), pan servo, tilt yoke, stereo camera bar, // ghost servo-controller + Pi 5. Nothing is mounted on the headset. // Units: mm. Z up. Pan axis = Z through origin. Tilt axis = X. Cameras look toward +Y. // Render a printable part: openscad -D 'part="base"' -o base.stl mb001_rig.scad // part = "assembly" | "base" | "yoke" | "cam_bar" // // SERVO NUMBERS: L,W,H from vendor pages (see research/MB-001-servos-power.md). // shaft_off (shaft position from the body end) and spline height are APPROXIMATE // concept values, NOT sourced -> check against vendor STEP/drawing before printing. // CAMERA NUMBERS: Brio 102 x 27 x 26.5 mm (Logitech datasheet). 38 mm module depth is NOT // sourced (assumed 15 mm incl. lens) -> measure the Quanrui USB48MP01-QFZ. part = "assembly"; cam_type = "module38"; // "module38" | "brio" servo_type = "sts3215"; // "mg90s" | "sh0257" | "mg996r" | "ds3218" | "sts3215" cam_spacing = 63; // centre-to-centre, mm. AVP IPD range 51..75 mm show_ghosts = true; // servos, cameras, Pi, PCA9685 in assembly view pan_deg = 0; // pose preview tilt_deg = 0; tripod_hole = 6.6; // 1/4"-20 clearance (6.35 nominal) use_nut_trap = true; $fn = 48; // ---------- servo table: [L, W, H, shaft_off(approx), flange_w, horn_d] ---------- function servo_dims(t) = t=="mg90s" ? [22.8, 12.2, 28.5, 5.5, 32.5, 20] : t=="sh0257" ? [22.8, 12.0, 25.4, 5.5, 32.5, 20] : t=="mg996r" ? [40.7, 19.7, 42.9, 10.0, 53.0, 25] : t=="ds3218" ? [40.0, 20.0, 40.5, 10.0, 54.0, 25] : /*sts3215*/ [45.2, 24.7, 35.0, 10.0, 45.2, 25]; function sv_L(t)=servo_dims(t)[0]; function sv_W(t)=servo_dims(t)[1]; function sv_H(t)=servo_dims(t)[2]; function sv_so(t)=servo_dims(t)[3]; function sv_horn(t)=servo_dims(t)[5]; // ---------- camera table: [w, h, depth] as mounted (portrait Brio so 63 mm spacing fits) ---------- module38_dims = [38, 38, 15]; brio_dims = [27, 102, 26.5]; // Brio rotated 90 deg: 102 mm tall, 27 wide function cam_dims() = cam_type=="brio" ? brio_dims : module38_dims; cw = cam_dims()[0]; ch = cam_dims()[1]; cd = cam_dims()[2]; // ---------- derived geometry ---------- sL = sv_L(servo_type); sW = sv_W(servo_type); sH = sv_H(servo_type); so = sv_so(servo_type); hd = sv_horn(servo_type); wall = 2.4; base_t = 6; deck_x = 100; deck_y0 = -112; deck_y1 = 46; // deck: x +-50, y -112..46 horn_t = 3; yoke_t = 4; arm_t = 4; bar_t = 4; gap = 1.0; bar_len = cam_spacing + cw + 2*10; // camera bar length along X bar_h = ch + 14; // pan servo: shaft at origin, body toward +X z_s = base_t + sH - 4; // spline tip height (body bottom on deck) z_y = z_s + horn_t + 1; // top of horn / underside of yoke plate arm_x = bar_len/2 + gap + arm_t/2; // arm centre-plane tilt_z = z_y + yoke_t + bar_h/2 + 6; // tilt axis height yoke_w = 2*arm_x + arm_t; // ---------- primitives ---------- module servo_body(t) { // shaft at origin pointing +Z, body below L=sv_L(t); W=sv_W(t); H=sv_H(t); s=sv_so(t); fw=servo_dims(t)[4]; color("#444") translate([-s, -W/2, -H+4]) cube([L, W, H-8]); // body color("#444") translate([-(fw-L)/2 - s, -W/2, -H+4+ (H-8)*0.62]) cube([fw, W, 2.5]); // flange color("#999") cylinder(d=6, h=6); // spline } module horn(d, t) { color("#ddd") cylinder(d=d, h=t); } module camera() { // origin at lens centre on rear face; looks +Y color("#222") translate([-cw/2, 0, -ch/2]) cube([cw, cd, ch]); color("#68c") translate([0, cd, 0]) rotate([-90,0,0]) cylinder(d=min(cw,ch)*0.45, h=3); } module m3_hole(h=40, d=3.3) cylinder(d=d, h=h, center=true, $fn=24); // ---------- BASE ---------- module base() { difference() { union() { translate([-deck_x/2, deck_y0, 0]) cube([deck_x, deck_y1-deck_y0, base_t]); // pan-servo cradle walls (servo body toward +X, shaft at origin) translate([-so-wall, -sW/2-wall, base_t]) difference() { cube([sL+2*wall, sW+2*wall, sH*0.55]); translate([wall, wall, -1]) cube([sL, sW, sH]); } } // 1/4"-20 tripod hole, centred on pan axis, with nut trap underneath translate([0,0,-1]) cylinder(d=tripod_hole, h=base_t+2); if (use_nut_trap) translate([0,0,-0.01]) cylinder(d=12.0, h=2.5, $fn=6); // trap: ~11 mm A/F nut (check) // desk screw slots for (sx=[-1,1], y=[deck_y0+8, deck_y1-8]) translate([sx*(deck_x/2-8), y, 0]) m3_hole(base_t*3, 4.2); // Pi 5 standoff holes (58 x 49 pattern per Raspberry Pi mechanical drawing), centred on deck at y=-70 for (dx=[-29,29], dy=[-24.5,24.5]) translate([dx, -70+dy, 0]) m3_hole(base_t*3, 2.7); // servo cable slot translate([-so+2, -sW/2, -1]) cube([6, sW, base_t+2]); } } // ---------- YOKE (pan platform + two arms, tilt servo on +X arm) ---------- module yoke() { difference() { union() { // pan plate (bolts to pan horn) translate([-yoke_w/2, -22, z_y]) cube([yoke_w, 44, yoke_t]); // arms for (sx=[-1,1]) translate([sx*arm_x - arm_t/2, -16, z_y]) cube([arm_t, 32, tilt_z - z_y + 14]); } // horn screw pattern (4x on r=8) for (a=[0:90:270]) translate([8*cos(a), 8*sin(a), z_y]) m3_hole(3*yoke_t, 2.6); // centre hole translate([0,0,z_y-1]) cylinder(d=7, h=yoke_t+2); // tilt pivot holes (x-axis): bearing/bolt side (-X) and servo spline side (+X) translate([0,0,tilt_z]) rotate([0,90,0]) cylinder(d=8.2, h=3*yoke_w, center=true); } // servo seat on +X arm outer face (servo body outward, shaft pointing -X) if (part!="assembly") translate([arm_x+arm_t/2, 0, tilt_z]) rotate([0,-90,0]) %servo_body(servo_type); } // ---------- CAMERA BAR (tilts about X axis through its centre) ---------- module cam_bar() { difference() { translate([-bar_len/2, -bar_t/2, -bar_h/2]) cube([bar_len, bar_t, bar_h]); // cable / lens relief windows for (sx=[-1,1]) translate([sx*cam_spacing/2, 0, 0]) cube([cw*0.5, 3*bar_t, ch*0.5], center=true); // centre axle hole for tilt pin (also horn screws on the servo end) rotate([0,90,0]) cylinder(d=3.3, h=bar_len+2, center=true, $fn=24); // M3 mounting holes at each camera (4 per camera at +-(cw/2-3), +-(min(ch,40)/2-3)) for (sx=[-1,1], hx=[-1,1], hz=[-1,1]) translate([sx*cam_spacing/2 + hx*(cw/2-3.5), 0, hz*(min(ch,38)/2-3.5)]) rotate([90,0,0]) m3_hole(3*bar_t, 2.9); } // end hubs that clamp onto the horn / bearing pin for (sx=[-1,1]) translate([sx*(bar_len/2), 0, 0]) rotate([0,90,0]) cylinder(d=14, h=2, center=true); } // ---------- GHOSTS (pose + non-printed parts) ---------- module ghosts() { // pan servo translate([0,0,z_s]) servo_body(servo_type); // horn translate([0,0,z_s+1]) horn(hd, horn_t); // Raspberry Pi 5 (85 x 56 mm board, ~20 mm tall with USB) on deck, y centre -70 color("#2a7", 0.35) translate([-28, -70-42.5, base_t+5]) cube([56, 85, 17]); // PCA9685 (approx 62 x 25 x 8) beside the Pi — approximate, unsourced size color("#d62", 0.45) translate([-31, -104, base_t+1]) cube([62, 14, 8]); // power: servo UBEC / bus adapter placeholder color("#c33", 0.4) translate([32, -60, base_t+1]) cube([14, 30, 10]); } module tilt_assembly() { translate([0,0,tilt_z]) rotate([tilt_deg,0,0]) { cam_bar(); if (show_ghosts) for (sx=[-1,1]) translate([sx*cam_spacing/2, bar_t/2, 0]) camera(); } if (show_ghosts) translate([arm_x+arm_t/2, 0, tilt_z]) rotate([0,-90,0]) servo_body(servo_type); } module assembly() { color("#b8c4d0") base(); rotate([0,0,pan_deg]) { color("#d9a05b") yoke(); tilt_assembly(); } if (show_ghosts) ghosts(); } if (part=="assembly") assembly(); else if (part=="base") base(); else if (part=="yoke") yoke(); else if (part=="cam_bar") rotate([90,0,0]) cam_bar(); // lay flat for printing // echo key numbers echo(str("bar_len=",bar_len," bar_h=",bar_h," tilt_z=",tilt_z," yoke_w=",yoke_w, " cam=",cam_type," servo=",servo_type," spacing=",cam_spacing));