Files
hexapod-robot/2026-current/hardware/openscad/body.scad
T
mangavalkandClaude Opus 5.5 91b5b99fd5 2026: OpenSCAD robot and remote, physical joint limits, model in Unity
hardware/openscad/ rebuilds the broken Fusion models as OpenSCAD:
body (top/bottom plate, standoffs, battery bay, switch), legs (coxa
bracket, femur link, tibia, horn adapters, TPU socks, servo cables),
the whole robot, and the remote (shell, lid, Nextion 3.5", JH-D400X-R4
sticks, 18650 UPS board, IMU, Teensy 4.0, USB-C breakout). Every part
was checked by slicing against the original STEP solids; the original's
bugs are fixed and listed in PLANNING.md.

The robot now uses a hexagon leg layout (corner legs 60 deg round from
the mid legs; the corner coxa brackets need turning on their splines).
Physical limits from a collision sweep of the model are in Config.h:
per-leg coxa limits and LEG_CLEARANCE_MM 56 (a leg is 55.2 mm wide at
the joints); the sim API exports them (version 7). sim_smoke passes.

Unity loads the model (export_unity.py -> Resources/OpenScadModel,
OpenScadModel.cs builds a new rig in place of the old scene's robot).
The PCB model's mounting holes now use the exact EasyEDA grid. README
shows renders of both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-14 12:14:32 +01:00

206 lines
7.9 KiB
OpenSCAD

// 2026 hexapod body: top + bottom plate, rebuilt from the Fusion 360 model
// fusion360/body-bottom/bottom v59.f3d (the `top` and `bottom` solids in
// "bottom v59.stp", which are newer than the top.dxf/bottom.dxf sketches).
//
// Plate geometry is written in the original's top-view coordinates
// (u, v) = (Fusion x, -Fusion z), so every number can be checked against
// the Fusion file. fusion_to_robot() turns that into the robot frame of
// robot/Config.h (+X forward, +Y left, +Z up): X = v, Y = -u.
// Z = 0 is the top face of the bottom plate.
//
// Deviations from the original (its bugs):
// - Left-front pivot boss at u = -50 instead of -50.894.
// - All six coxa servos present (the original has none at right front).
// - Hex nut wells share one orientation (the original's are rotated randomly).
use <../main-pcb-2026-11-13/main-pcb.scad>
include <servo.scad>
/* [Plates] */
PLATE_T = 3.0; // original: 3 mm, both plates
CAVITY_H = 27.0; // inner gap; sized so the servo case ends 2 mm above the bottom plate
/* [Standoffs: split halfway, screw from the top into a captive nut] */
STANDOFF_D = 15.0;
SCREW_D = 3.1;
SCREW_GRIP = 4.0; // solid length of each half next to the split
COUNTERBORE_D = 7.0;
NUT_AF = 5.5; // M3 nut across flats
STANDOFFS_UV = [[35, 30], [-35, 30], [35, -30], [-35, -30]];
/* [Coxa servos] */
// Shaft axis per leg, in (u, v), with the direction the case extends from the shaft.
// Indices match robot/Config.h legs 0..5.
COXA_UV = [
[ 50, 100, 230], // 0 right front
[ 65, 0, 180], // 1 right mid
[ 50, -100, 130], // 2 right rear
[-50, -100, 50], // 3 left rear
[-65, 0, 0], // 4 left mid
[-50, 100, -50], // 5 left front (original boss at u = -50.894)
];
ARM_R = 19.5; // rounded arm end, centred on the coxa axis
PIVOT_BOSS_D = 14.0; // under the bottom plate, carries the coxa bracket
PIVOT_BOSS_H = 2.0;
/* [Teensy, PCB and switch] */
TEENSY_CUT = [23.78, 66.96]; // (u, v) size, centred
PCB_BOSSES_UV = [[17.89, 39.48], [-17.89, 39.48], [17.89, -39.48], [-17.89, -39.48]];
PCB_BOSS_D = 5.0;
PCB_BOSS_H = 1.0; // PCB top face ends up 1 mm under the top plate
PCB_SCREW_D = 2.5;
SWITCH_SLOT = [[-4, 57], [4, 73]];
SWITCH_HOLES = [[0, 55.5], [0, 74.5]];
SWITCH_HOLE_D = 2.0;
ENGRAVE_D = 1.0;
/* [Battery bay on the bottom plate] */
BAY_RAIL_U = [26.06, 29.06]; // inner/outer face of each rail, mirrored in u
BAY_V = [-74.21, 31.79]; // open end at -v (rear), closed end at +v
BAY_STOP = [[-16, 28.79], [16, 31.79]];
BAY_H = 13.5;
/* [Rendering] */
SHOW_TOP = true;
SHOW_BOTTOM = true;
SHOW_PCB = true;
SHOW_SERVOS = true;
EXPLODE = 0; // mm; lifts the top half
PCB_USB_FRONT = false; // owner: Teensy USB faces away from the switch (rear)
/* [Hidden] */
$fa = 2; $fs = 0.3;
STANDOFF_SPLIT = CAVITY_H / 2;
TOP_Z = CAVITY_H + PLATE_T;
module fusion_to_robot() rotate([0, 0, -90]) children();
// linear_extrude with a convexity, so the preview shows holes correctly.
module lx(h) linear_extrude(h, convexity = 10) children();
// ---------- outline -------------------------------------------------------
function bez2(p, t) = (1-t)*(1-t)*p[0] + 2*(1-t)*t*p[1] + t*t*p[2];
function rev(l) = [for (i = [len(l)-1:-1:0]) l[i]];
NOTCH = [[-34.12, 111.31], [0, 71.93], [34.12, 111.31]]; // front/back notch
WAIST = [[60.03, 83.28], [49.86, 74.48], [50.13, 64.77]]; // corner arm -> side
FILLET_C = [65, 35];
FILLET_R = 15.5;
N = 24;
// Right half, front to back; the arm circles fill in the chords at the arm ends.
Q1 = concat(
[for (i = [N/2:N]) bez2(NOTCH, i/N)],
[for (i = [0:N]) bez2(WAIST, i/N)],
[[49.5, 35]],
[for (a = [180:5:270]) FILLET_C + FILLET_R * [cos(a), sin(a)]],
[[65, 0]]);
RIGHT = concat(Q1, [for (p = rev(Q1)) [p[0], -p[1]]]);
OUTLINE = concat(RIGHT, [for (p = rev(RIGHT)) [-p[0], p[1]]]);
module outline_2d() {
polygon(OUTLINE);
for (c = COXA_UV) translate([c[0], c[1]]) circle(r = ARM_R);
}
module at_coxa(c) translate([c[0], c[1]]) rotate([0, 0, c[2]]) children();
// ---------- top plate -----------------------------------------------------
module top_plate_2d() {
difference() {
outline_2d();
for (c = COXA_UV) at_coxa(c) servo_pocket_2d();
square(TEENSY_CUT, center = true);
for (s = STANDOFFS_UV) translate(s) circle(d = SCREW_D);
for (b = PCB_BOSSES_UV) translate(b) circle(d = PCB_SCREW_D);
translate(SWITCH_SLOT[0]) square(SWITCH_SLOT[1] - SWITCH_SLOT[0]);
for (h = SWITCH_HOLES) translate(h) circle(d = SWITCH_HOLE_D);
}
}
module switch_text() {
for (t = [["ON", 49.3], ["OFF", 80.6]])
translate([0, t[1]])
text(t[0], size = 5.8, halign = "center", valign = "center",
font = "Liberation Sans");
}
module top_half() render(convexity = 10) {
difference() {
union() {
translate([0, 0, CAVITY_H]) lx(PLATE_T) top_plate_2d();
for (s = STANDOFFS_UV) translate([s[0], s[1], STANDOFF_SPLIT])
cylinder(d = STANDOFF_D, h = CAVITY_H - STANDOFF_SPLIT);
for (b = PCB_BOSSES_UV) translate([b[0], b[1], CAVITY_H - PCB_BOSS_H])
cylinder(d = PCB_BOSS_D, h = PCB_BOSS_H);
}
for (s = STANDOFFS_UV) translate([s[0], s[1], 0]) {
translate([0, 0, STANDOFF_SPLIT - 1]) cylinder(d = SCREW_D, h = SCREW_GRIP + 2);
translate([0, 0, STANDOFF_SPLIT + SCREW_GRIP]) cylinder(d = COUNTERBORE_D, h = TOP_Z);
}
for (b = PCB_BOSSES_UV) translate([b[0], b[1], 0]) {
translate([0, 0, CAVITY_H - PCB_BOSS_H - 1]) cylinder(d = PCB_SCREW_D, h = PLATE_T + 2);
translate([0, 0, TOP_Z - 1]) cylinder(d = PCB_BOSS_D, h = 2);
}
translate([0, 0, TOP_Z - ENGRAVE_D]) lx(ENGRAVE_D + 1) switch_text();
}
}
// ---------- bottom plate --------------------------------------------------
module bay_2d() {
for (m = [1, -1]) scale([m, 1])
translate([BAY_RAIL_U[0], BAY_V[0]])
square([BAY_RAIL_U[1] - BAY_RAIL_U[0], BAY_V[1] - BAY_V[0]]);
translate(BAY_STOP[0]) square(BAY_STOP[1] - BAY_STOP[0]);
}
module bottom_half() render(convexity = 10) {
difference() {
union() {
translate([0, 0, -PLATE_T]) lx(PLATE_T) outline_2d();
for (s = STANDOFFS_UV) translate([s[0], s[1], 0])
cylinder(d = STANDOFF_D, h = STANDOFF_SPLIT);
lx(BAY_H) bay_2d();
for (c = COXA_UV) translate([c[0], c[1], -PLATE_T - PIVOT_BOSS_H])
cylinder(d = PIVOT_BOSS_D, h = PIVOT_BOSS_H);
}
for (s = STANDOFFS_UV) translate([s[0], s[1], 0]) {
translate([0, 0, STANDOFF_SPLIT - SCREW_GRIP - 1]) cylinder(d = SCREW_D, h = SCREW_GRIP + 2);
translate([0, 0, -PLATE_T - 1])
cylinder(d = NUT_AF / cos(30), h = STANDOFF_SPLIT - SCREW_GRIP + PLATE_T + 1, $fn = 6);
}
}
}
// ---------- PCB -----------------------------------------------------------
// Long axis along the body (robot X), holes on the PCB bosses, top face
// 1 mm under the top plate, Teensy up through the cut-out.
PCB_T = 1.6;
PCB_HOLE_CENTER = [22.89, 44.52]; // HOLE_GRID_CENTER in main-pcb.scad
module pcb_in_body() {
translate([0, 0, CAVITY_H - PCB_BOSS_H - PCB_T])
rotate([0, 0, PCB_USB_FRONT ? -90 : 90])
translate([-PCB_HOLE_CENTER[0], -PCB_HOLE_CENTER[1], 0])
main_pcb();
}
// ---------- assembly ------------------------------------------------------
module body_servos_uv() for (c = COXA_UV) translate([0, 0, TOP_Z]) at_coxa(c) hs485hb();
module body_assembly() {
fusion_to_robot() {
if (SHOW_BOTTOM) color("#3a3a3a") bottom_half();
if (SHOW_TOP) translate([0, 0, EXPLODE]) color("#555555") top_half();
if (SHOW_SERVOS) translate([0, 0, EXPLODE]) body_servos_uv();
}
if (SHOW_PCB) translate([0, 0, EXPLODE]) pcb_in_body();
}
body_assembly();