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>
384 lines
15 KiB
OpenSCAD
384 lines
15 KiB
OpenSCAD
// 2026 hexapod leg, rebuilt from the Fusion 360 model
|
|
// fusion360/body-bottom/bottom v59.f3d (bottom-leg v31 + hinge1/hinge2).
|
|
//
|
|
// Leg frame (left leg): origin on the coxa axis at the joint plane
|
|
// (13.5 mm above the bottom plate's top face), +x outward along the leg,
|
|
// +y along the femur/knee axes towards the horn side (the robot's rear),
|
|
// +z up. Right legs are the mirror image (see robot.scad).
|
|
// leg(pose) poses it with the firmware's joint convention (robot/Config.h):
|
|
// coxa + = CCW seen from above, femur + = up, tibia relative to the femur,
|
|
// + = up. The original is modelled at [0, 0, -90].
|
|
//
|
|
// Every joint uses the same layout: servo ears 16.5 mm from the joint
|
|
// plane; a 5 mm horn plate at 28.7..33.7 on one side holding a printed
|
|
// horn adapter; a 3 mm bearing plate at -21.5..-18.5 on the other side,
|
|
// whose ring takes a 14 mm boss on the opposite part; plates shaped as a
|
|
// lens (r 25.5 around the axis, cut by r 19.963 offset 9.463 towards the
|
|
// part's body); and a wrap shell up to r 25.5 for clearance.
|
|
//
|
|
// Deviations from the original (its bugs):
|
|
// - Tibia halves split exactly at y = 0 (one half was 0.1 mm off, holes up
|
|
// to 0.03 mm apart, different blade spline nets).
|
|
// - Lid locating lips all 0.8 mm (the original's were 0.5..0.9 mm).
|
|
// - The foot sock sits on the foot (the original's was 10/13.5 mm off).
|
|
|
|
include <servo.scad>
|
|
|
|
/* [Links (match robot/Config.h)] */
|
|
COXA_LEN = 55;
|
|
FEMUR_LEN = 100;
|
|
TIBIA_LEN = 150; // knee axis to foot ball centre
|
|
|
|
/* [Shared joint design] */
|
|
SHELL_R = 25.5;
|
|
BRACKET_SHELL_IN = 23.5;
|
|
TIBIA_SHELL_IN = 24.0;
|
|
LENS_R = 19.963;
|
|
LENS_OFFSET = 9.463;
|
|
HORN_PLATE = [28.7, 33.7];
|
|
BEARING_PLATE = [-21.5, -18.5];
|
|
RING = [7.2, 10.5, 1.5]; // pocket radius, outer radius, height
|
|
BOSS_D = 14;
|
|
BOSS_H = 2;
|
|
SERVO_EAR_PLANE = 16.5;
|
|
HORN_SQUARE = 30;
|
|
JOIN = 1; // shells reach this far into their plates, so parts fuse
|
|
HORN_HUB_D = 9.2;
|
|
HORN_CROSS_DEPTH = 2;
|
|
|
|
/* [Fasteners] */
|
|
M3_D = 3.1;
|
|
M3_HEAD_D = 5.7;
|
|
M3_NUT_AF = 5.7;
|
|
|
|
/* [Coxa bracket] */
|
|
BLOCK_Z = [-12, 12.5];
|
|
BLOCK_Y = 15;
|
|
BRACKET_BOLTS = [[26.4, 10.92], [26.4, -10.92]]; // (x, y), vertical
|
|
CABLE_D = 6;
|
|
|
|
/* [Femur link] */
|
|
LINK_X = [-19, 119];
|
|
LINK_Z = 13; // half height
|
|
LINK_R = 3;
|
|
LINK_WALL = 2;
|
|
LINK_TOP = [13.5, 16.5]; // top wall, carries both servos
|
|
LINK_LID = [-16.5, -13.5];
|
|
LIP = [0.8, 3, 0.5]; // thickness, height, clearance to the wall
|
|
CABLE_SLOT = [-12.5, 3]; // slot for x < -12.5, |y| < 3
|
|
|
|
/* [Tibia] */
|
|
LOBE_X = 25.5;
|
|
LOBE_R = 14.985;
|
|
LOBE_Y = 12;
|
|
TIBIA_BOLTS = [[26.43, 10.585], [26.43, -10.585]]; // (x, z), along y
|
|
ROD_R = 5;
|
|
ROD_PATH = [[28.5, -0], [47.41, -36.13], [0, -TIBIA_LEN]]; // (x, z), quadratic Bezier
|
|
|
|
/* [Foot sock (TPU)] */
|
|
SOCK_T = 0.5; // wall, same as the real printed socks
|
|
SOCK_LEN = 12.2; // along the rod from the foot ball centre
|
|
|
|
/* [Rendering] */
|
|
SHOW_SERVOS = true;
|
|
SHOW_ADAPTERS = true;
|
|
SHOW_SOCK = true;
|
|
SHOW_CABLES = true;
|
|
|
|
/* [Hidden] */
|
|
$fa = 2; $fs = 0.3;
|
|
C_BRACKET = "#d9822b";
|
|
C_LINK = "#4a7fbf";
|
|
C_TIBIA = "#5fa35f";
|
|
C_ADAPTER = "#bdbdbd";
|
|
C_SOCK = "#1c1c1c";
|
|
BIG = 200;
|
|
|
|
// ---------- helpers -------------------------------------------------------
|
|
|
|
function rot2(p, a) = [p[0]*cos(a) - p[1]*sin(a), p[0]*sin(a) + p[1]*cos(a)];
|
|
function bez(p, t) = (1-t)*(1-t)*p[0] + 2*(1-t)*t*p[1] + t*t*p[2];
|
|
|
|
// Shell around the local Z axis between r_in and SHELL_R, ±45° about angle a.
|
|
module shell(r_in, z0, z1, a = 0)
|
|
translate([0, 0, z0]) rotate([0, 0, a - 45])
|
|
lx(z1 - z0) intersection() {
|
|
difference() { circle(r = SHELL_R); circle(r = r_in); }
|
|
polygon([[0, 0], [2 * SHELL_R, 0], [2 * SHELL_R, 2 * SHELL_R], [0, 2 * SHELL_R]]);
|
|
}
|
|
|
|
// linear_extrude with a convexity, so the preview shows holes and shells.
|
|
module lx(h) linear_extrude(h, convexity = 10) children();
|
|
|
|
// Plate outline around an axis at the origin, biased by `off` along x.
|
|
module lens_2d(off) intersection() {
|
|
circle(r = SHELL_R);
|
|
translate([off, 0]) circle(r = LENS_R);
|
|
}
|
|
|
|
// Turns a part built along local Z into one along +y (for horizontal axes).
|
|
module along_y(y0) translate([0, y0, 0]) rotate([-90, 0, 0]) children();
|
|
|
|
module ring(z0) translate([0, 0, z0]) difference() {
|
|
cylinder(r = RING[1], h = RING[2]);
|
|
translate([0, 0, -1]) cylinder(r = RING[0], h = RING[2] + 2);
|
|
}
|
|
|
|
module hex_well(af, h) cylinder(d = af / cos(30), h = h, $fn = 6);
|
|
|
|
// The servo cross horn, arms on the diagonals (same in every horn pocket).
|
|
CROSS_ARM = concat([[6.106, 0], [10.926, 6.853]],
|
|
[for (a = [-34.56 : 10 : 125.44]) [8.874, 8.874] + 2.88 * [cos(a), sin(a)]],
|
|
[[6.853, 10.926]]);
|
|
module cross_2d() polygon([for (k = [0:3]) for (p = CROSS_ARM) rot2(p, 90 * k)]);
|
|
|
|
// Hub hole through, cross pocket in the top HORN_CROSS_DEPTH of a 5 mm plate.
|
|
module horn_cut(h = 5) {
|
|
translate([0, 0, -1]) cylinder(d = HORN_HUB_D, h = h + 2);
|
|
translate([0, 0, h - HORN_CROSS_DEPTH]) lx(HORN_CROSS_DEPTH + 1) cross_2d();
|
|
}
|
|
|
|
// Printed horn adapter ("servo horn v6"): sits in a horn plate's square window.
|
|
module horn_adapter() color(C_ADAPTER) render(convexity = 10) difference() {
|
|
translate([-HORN_SQUARE/2, -HORN_SQUARE/2, 0]) cube([HORN_SQUARE, HORN_SQUARE, 5]);
|
|
horn_cut();
|
|
}
|
|
|
|
// ---------- coxa bracket --------------------------------------------------
|
|
// Two printed halves split at the joint plane, bolted with 2 x M3.
|
|
|
|
// Fills the gap between the two shells; reaches 1 mm into both shell walls so
|
|
// each bracket half prints as one solid (in the original they are separate bodies).
|
|
module bracket_block(z0, z1) difference() {
|
|
translate([18, -BLOCK_Y, z0]) cube([20, 2 * BLOCK_Y, z1 - z0]);
|
|
cylinder(r = SHELL_R - 1, h = BIG, center = true);
|
|
translate([COXA_LEN, 0, 0]) rotate([90, 0, 0]) cylinder(r = SHELL_R - 1, h = BIG, center = true);
|
|
}
|
|
|
|
module bracket_common_cuts() {
|
|
translate([20, 0, 0]) rotate([0, 90, 0]) cylinder(d = CABLE_D, h = 15);
|
|
for (b = BRACKET_BOLTS) translate([b[0], b[1], -5]) cylinder(d = M3_D, h = 10);
|
|
}
|
|
|
|
module coxa_bracket_upper() color(C_BRACKET) render(convexity = 10) difference() {
|
|
union() {
|
|
shell(BRACKET_SHELL_IN, 0, HORN_PLATE[0] + JOIN);
|
|
translate([0, 0, HORN_PLATE[0]]) difference() {
|
|
lx(HORN_PLATE[1] - HORN_PLATE[0]) intersection() {
|
|
circle(r = SHELL_R);
|
|
union() {
|
|
square(HORN_SQUARE, center = true);
|
|
translate([LENS_OFFSET, 0]) circle(r = LENS_R);
|
|
}
|
|
}
|
|
horn_cut();
|
|
}
|
|
bracket_block(0, BLOCK_Z[1]);
|
|
translate([COXA_LEN, 0, 0]) {
|
|
along_y(0) shell(BRACKET_SHELL_IN, 0, HORN_PLATE[0] + JOIN, 180);
|
|
along_y(HORN_PLATE[0]) lx(HORN_PLATE[1] - HORN_PLATE[0]) difference() {
|
|
lens_2d(-LENS_OFFSET);
|
|
square(HORN_SQUARE, center = true);
|
|
}
|
|
}
|
|
}
|
|
bracket_common_cuts();
|
|
for (b = BRACKET_BOLTS) translate([b[0], b[1], 4]) cylinder(d = M3_HEAD_D, h = 10);
|
|
}
|
|
|
|
module coxa_bracket_lower() color(C_BRACKET) render(convexity = 10) difference() {
|
|
union() {
|
|
shell(BRACKET_SHELL_IN, BEARING_PLATE[1] - JOIN, 0);
|
|
translate([0, 0, BEARING_PLATE[0]])
|
|
lx(BEARING_PLATE[1] - BEARING_PLATE[0]) lens_2d(LENS_OFFSET);
|
|
ring(BEARING_PLATE[1]);
|
|
bracket_block(BLOCK_Z[0], 0);
|
|
translate([COXA_LEN, 0, 0]) {
|
|
along_y(BEARING_PLATE[1] - JOIN) shell(BRACKET_SHELL_IN, 0, -BEARING_PLATE[1] + JOIN, 180);
|
|
along_y(BEARING_PLATE[0]) lx(BEARING_PLATE[1] - BEARING_PLATE[0])
|
|
lens_2d(-LENS_OFFSET);
|
|
along_y(BEARING_PLATE[1]) ring(0);
|
|
}
|
|
}
|
|
bracket_common_cuts();
|
|
for (b = BRACKET_BOLTS) translate([b[0], b[1], BLOCK_Z[0] - 1]) hex_well(M3_NUT_AF, 9);
|
|
}
|
|
|
|
// ---------- femur link ----------------------------------------------------
|
|
// Femur frame: origin on the femur axis, knee at x = FEMUR_LEN.
|
|
|
|
module link_profile_2d(inset = 0, r = LINK_R)
|
|
translate([LINK_X[0] + inset, -LINK_Z + inset])
|
|
offset(r = r) offset(delta = -r)
|
|
square([LINK_X[1] - LINK_X[0] - 2*inset, 2*(LINK_Z - inset)]);
|
|
|
|
SERVO_SHAFTS = [[0, 0], [FEMUR_LEN, 180]]; // (x, direction the case extends)
|
|
|
|
module link_channel() color(C_LINK) render(convexity = 10) difference() {
|
|
along_y(LINK_LID[1]) lx(LINK_TOP[1] - LINK_LID[1]) link_profile_2d();
|
|
along_y(LINK_LID[1] - 1) lx(LINK_TOP[0] - LINK_LID[1] + 1)
|
|
link_profile_2d(LINK_WALL, 1);
|
|
for (s = SERVO_SHAFTS) along_y(LINK_TOP[0] - 1) translate([s[0], 0]) rotate([0, 0, s[1]])
|
|
lx(LINK_TOP[1] - LINK_TOP[0] + 2) servo_pocket_2d();
|
|
translate([LINK_X[0] - 1, -CABLE_SLOT[1], -BIG/2])
|
|
cube([CABLE_SLOT[0] - LINK_X[0] + 1, 2 * CABLE_SLOT[1], BIG]);
|
|
}
|
|
|
|
module link_lid() color(C_LINK) render(convexity = 10) {
|
|
along_y(LINK_LID[0]) lx(LINK_LID[1] - LINK_LID[0]) link_profile_2d();
|
|
for (x = [0, FEMUR_LEN]) translate([x, 0, 0])
|
|
along_y(LINK_LID[0] - BOSS_H) cylinder(d = BOSS_D, h = BOSS_H);
|
|
in = LINK_WALL + LIP[2];
|
|
along_y(LINK_LID[1]) lx(LIP[1]) {
|
|
for (m = [1, -1]) scale([1, m])
|
|
translate([-10, LINK_Z - in - LIP[0]]) square([120, LIP[0]]);
|
|
translate([LINK_X[0] + in, -8]) square([LIP[0], 16]);
|
|
translate([LINK_X[1] - in - LIP[0], -8]) square([LIP[0], 16]);
|
|
}
|
|
}
|
|
|
|
module link_servos() if (SHOW_SERVOS)
|
|
for (s = SERVO_SHAFTS) translate([s[0], SERVO_EAR_PLANE, 0])
|
|
rotate([-90, 0, 0]) rotate([0, 0, s[1]]) hs485hb();
|
|
|
|
// ---------- tibia ---------------------------------------------------------
|
|
// Knee frame: origin on the knee axis, axes as the femur frame in the
|
|
// original pose (tibia at -90 degrees, rod hanging down).
|
|
|
|
ROD_N = 30;
|
|
module rod() for (i = [0:ROD_N-1]) hull() {
|
|
a = bez(ROD_PATH, i / ROD_N); b = bez(ROD_PATH, (i + 1) / ROD_N);
|
|
translate([a[0], 0, a[1]]) sphere(r = ROD_R, $fn = 36);
|
|
translate([b[0], 0, b[1]]) sphere(r = ROD_R, $fn = 36);
|
|
}
|
|
|
|
module half_space(side) translate([-BIG, side > 0 ? 0 : -BIG, -BIG]) cube([2*BIG, BIG, 2*BIG]);
|
|
|
|
// Bolt lobe behind the knee, only outside the shell (r > SHELL_R), so it
|
|
// clears the femur link's end at any knee angle.
|
|
module tibia_lobe(y0, y1) along_y(y0) lx(y1 - y0) difference() {
|
|
translate([LOBE_X, 0]) circle(r = LOBE_R);
|
|
circle(r = SHELL_R - JOIN);
|
|
}
|
|
|
|
// One half of the rod, trimmed to stay outside the shell's inner radius.
|
|
module rod_half(side) difference() {
|
|
intersection() { rod(); half_space(side); }
|
|
along_y(-BIG/2) cylinder(r = TIBIA_SHELL_IN, h = BIG);
|
|
}
|
|
|
|
module tibia_bolts(y0, h, d) for (b = TIBIA_BOLTS) translate([b[0], 0, b[1]]) along_y(y0) cylinder(d = d, h = h);
|
|
|
|
// Bearing side (original "leg left 1"); M3 heads in its 8 mm counterbores.
|
|
module tibia_bearing_half() color(C_TIBIA) render(convexity = 10) difference() {
|
|
union() {
|
|
along_y(BEARING_PLATE[0]) lx(BEARING_PLATE[1] - BEARING_PLATE[0]) lens_2d(LENS_OFFSET);
|
|
along_y(BEARING_PLATE[1]) ring(0);
|
|
along_y(BEARING_PLATE[1] - JOIN) shell(TIBIA_SHELL_IN, 0, -BEARING_PLATE[1] + JOIN);
|
|
tibia_lobe(-LOBE_Y, 0);
|
|
rod_half(-1);
|
|
}
|
|
tibia_bolts(-4, 5, M3_D);
|
|
tibia_bolts(-LOBE_Y - 1, LOBE_Y - 4 + 1, M3_HEAD_D);
|
|
}
|
|
|
|
// Horn side (original "leg left 2"); M3 nuts in its hex wells.
|
|
module tibia_horn_half() color(C_TIBIA) render(convexity = 10) difference() {
|
|
union() {
|
|
along_y(HORN_PLATE[0]) lx(HORN_PLATE[1] - HORN_PLATE[0]) difference() {
|
|
lens_2d(LENS_OFFSET);
|
|
square(HORN_SQUARE, center = true);
|
|
}
|
|
along_y(0) shell(TIBIA_SHELL_IN, 0, HORN_PLATE[0] + JOIN);
|
|
tibia_lobe(0, LOBE_Y);
|
|
rod_half(1);
|
|
}
|
|
tibia_bolts(-1, 5, M3_D);
|
|
for (b = TIBIA_BOLTS) translate([b[0], 0, b[1]]) along_y(4) hex_well(M3_NUT_AF, LOBE_Y);
|
|
}
|
|
|
|
// TPU sock over the foot ball and the end of the rod.
|
|
FOOT = [ROD_PATH[2][0], 0, ROD_PATH[2][1]];
|
|
SOCK_DIR = [ROD_PATH[1][0] - ROD_PATH[2][0], 0, ROD_PATH[1][1] - ROD_PATH[2][1]]
|
|
/ norm([ROD_PATH[1][0] - ROD_PATH[2][0], ROD_PATH[1][1] - ROD_PATH[2][1]]);
|
|
module foot_sock() color(C_SOCK) render(convexity = 10) difference() {
|
|
intersection() {
|
|
hull() {
|
|
translate(FOOT) sphere(r = ROD_R + SOCK_T);
|
|
translate(FOOT + SOCK_LEN * SOCK_DIR) sphere(r = ROD_R + SOCK_T);
|
|
}
|
|
// flat open end, perpendicular to the rod
|
|
translate(FOOT) rotate([0, atan2(SOCK_DIR[0], SOCK_DIR[2]), 0])
|
|
translate([-BIG/2, -BIG/2, -BIG]) cube([BIG, BIG, BIG + SOCK_LEN]);
|
|
}
|
|
rod();
|
|
}
|
|
|
|
// ---------- servo cables (reference only) ---------------------------------
|
|
// Route of the original: along the lid under the servo cases, up through
|
|
// the link's end slot, through the bracket's cable channel, into the body
|
|
// cavity at the arm end (the channel sits at the cavity's mid-height).
|
|
// The lead leaves the case at the end opposite the shaft, low down.
|
|
|
|
CABLE_W = 2.4;
|
|
C_CABLE = "#5a3a1e";
|
|
|
|
module cable(pts) color(C_CABLE)
|
|
for (i = [0:len(pts)-2]) hull() {
|
|
translate(pts[i]) sphere(d = CABLE_W, $fn = 12);
|
|
translate(pts[i+1]) sphere(d = CABLE_W, $fn = 12);
|
|
}
|
|
|
|
// Femur frame part of a lead: case exit -> along the lid -> out of the end slot.
|
|
function link_lead(exit_x, toward, z) = [
|
|
[exit_x, -9, z], [exit_x + toward * 2, -12.3, z], [-12, -12.3, z],
|
|
[-15.5, -8, z * 0.5], [-17, 0, 0], [LINK_X[0] - 4, 0, 0]];
|
|
// Leg frame part, original pose: bracket channel -> into the body cavity.
|
|
LEAD_BODY = [[COXA_LEN - 25.5 - 1, 0, 0], [16, 0, 0], [12, 12, 0], [-35, 12, 0]];
|
|
|
|
// Coxa-frame part: bridge from the link's end slot to the bracket channel,
|
|
// then on into the body.
|
|
module coxa_cables() {
|
|
cable(LEAD_BODY);
|
|
cable([[COXA_LEN + LINK_X[0] - 4, 0, 0], [COXA_LEN - 25.5 - 1, 0, 0]]);
|
|
}
|
|
|
|
// Femur-frame part: both leads inside the femur link.
|
|
module femur_cables() {
|
|
cable(link_lead(30, 1, -3)); // femur servo
|
|
cable(link_lead(FEMUR_LEN - 30, -1, 3)); // knee servo
|
|
}
|
|
|
|
module leg_cables(pose) rotate([0, 0, pose[0]]) {
|
|
coxa_cables();
|
|
translate([COXA_LEN, 0, 0]) rotate([0, -pose[1], 0]) femur_cables();
|
|
}
|
|
|
|
// ---------- posed leg -----------------------------------------------------
|
|
|
|
module leg(pose = [0, 0, -90]) {
|
|
if (SHOW_CABLES) leg_cables(pose);
|
|
leg_parts(pose);
|
|
}
|
|
|
|
module leg_parts(pose) rotate([0, 0, pose[0]]) {
|
|
coxa_bracket_upper();
|
|
coxa_bracket_lower();
|
|
translate([COXA_LEN, 0, 0]) {
|
|
if (SHOW_ADAPTERS) along_y(HORN_PLATE[0]) horn_adapter();
|
|
rotate([0, -pose[1], 0]) {
|
|
link_channel();
|
|
link_lid();
|
|
link_servos();
|
|
translate([FEMUR_LEN, 0, 0]) rotate([0, -(pose[2] + 90), 0]) {
|
|
tibia_bearing_half();
|
|
tibia_horn_half();
|
|
if (SHOW_ADAPTERS) along_y(HORN_PLATE[0]) horn_adapter();
|
|
if (SHOW_SOCK) foot_sock();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
leg();
|