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>
This commit is contained in:
1 parent
b1c94e4149
commit
91b5b99fd5
42 files changed
+1560
-20
No files matched your search
@@ -68,8 +68,9 @@ y_mm = (3330.7087 - y_eda) * 0.254
|
||||
|
||||
- Outline: 46.0 × 89.0 mm, black soldermask, 1.6 mm FR4 (the OBJ's 2.54 mm
|
||||
"slab" is just the export; the physical board is standard).
|
||||
- Four M3 clearance holes (⌀ 4 mm), 5 mm in from each edge (hole grid
|
||||
35.78 × 78.96 mm).
|
||||
- Four M3 clearance holes (⌀ 4 mm) at (5.00, 5.04), (40.78, 5.04),
|
||||
(5.00, 84.00), (40.78, 84.00): hole grid 35.78 × 78.96 mm, centred at
|
||||
(22.89, 44.52), slightly off the board centre (23, 44.5).
|
||||
- Component placements (pad-group centres from the EasyEDA JSON — these
|
||||
are the exact (x, y) the SCAD uses). All in mm from the board's
|
||||
bottom-left corner.
|
||||
|
||||
@@ -21,7 +21,10 @@ CORNER_R = 1.0;
|
||||
|
||||
/* [Mounting holes] */
|
||||
HOLE_DIA = 4.0; // M3 clearance
|
||||
HOLE_INSET = 5.0;
|
||||
// Exact EasyEDA HOLE positions; the right/bottom insets are 5.22/5.04 mm,
|
||||
// not 5, so the grid is 35.78 x 78.96 mm (the body's PCB bosses use it).
|
||||
HOLES = [[5.00, 5.04], [40.78, 5.04], [5.00, 84.00], [40.78, 84.00]];
|
||||
HOLE_GRID_CENTER = [22.89, 44.52];
|
||||
|
||||
/* [Rendering] */
|
||||
SHOW_COMPONENTS = true;
|
||||
@@ -55,9 +58,7 @@ module board_outline_2d() {
|
||||
}
|
||||
|
||||
module mounting_holes_2d() {
|
||||
for (x = [HOLE_INSET, BOARD_W - HOLE_INSET],
|
||||
y = [HOLE_INSET, BOARD_H - HOLE_INSET])
|
||||
translate([x, y]) circle(d = HOLE_DIA);
|
||||
for (h = HOLES) translate(h) circle(d = HOLE_DIA);
|
||||
}
|
||||
|
||||
module pcb() {
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
# OpenSCAD body redesign: notes
|
||||
|
||||
`body.scad` rebuilds the body of `../fusion360/body-bottom/bottom v59.f3d`
|
||||
(top + bottom plate, PCB, coxa servos; legs are placeholders).
|
||||
|
||||
## Source of truth
|
||||
|
||||
The `top` and `bottom` **solids** in `bottom v59.stp` (inside
|
||||
`bottom v59.f3d_files.zip`). The `top.dxf` / `bottom.dxf` sketches in the
|
||||
same zip are older and differ (front/back notch, side curve, one servo
|
||||
pocket with an extra notch), so they are not used.
|
||||
|
||||
Coordinates: the plates are written in the original's top-view frame
|
||||
(u, v) = (Fusion x, -Fusion z); `fusion_to_robot()` rotates that by -90°
|
||||
into the robot frame of `robot/Config.h` (X = v forward, Y = -u left).
|
||||
Fusion's up axis is Y; Fusion Y = 0 is the top face of the bottom plate,
|
||||
which is also Z = 0 here.
|
||||
|
||||
`body.scad` slices were overlaid on the original solid edges at every
|
||||
feature height (plate faces, standoff split, counterbore floor, nut wells,
|
||||
PCB bosses, pivot bosses, battery bay): all match, apart from the fixes
|
||||
below.
|
||||
|
||||
## What the original body is
|
||||
|
||||
- Two 3 mm plates, cavity 27 mm (Z 0..27, top plate Z 27..30).
|
||||
- Outline: arm ends r 19.5 around each coxa axis, concave fillets r 15.5
|
||||
at the mid arms, quadratic curves at the sides and a notch front/back.
|
||||
Fully symmetric (the "asymmetry" seen earlier came from a stray line
|
||||
near (128, 8) in `bottom.dxf` and construction points at x = 100).
|
||||
- Coxa axes (u, v): corners (±50, ±100), mid (±65, 0) = robot
|
||||
(±100, ±50) and (0, ±65), same as `Config.h`.
|
||||
- Coxa servos (HS-485HB): ears rest on the top plate (Z 30), case hangs
|
||||
in a 41 × 21 pocket down to Z 2. Shaft 10 mm from the outer case end.
|
||||
Ear screw holes ⌀2.5, 48.25 × 10 mm. Corner servos turned 50°.
|
||||
- Under each coxa axis a ⌀14 × 2 mm pivot boss on the bottom plate; the
|
||||
coxa bracket (`hinge1` above / `hinge2` below, leg parts) wraps the
|
||||
plate edge between the servo horn and this boss.
|
||||
- 4 standoffs ⌀15 at (u, v) (±35, ±30), split at Z 13.5: M3 from the top
|
||||
in a ⌀7 counterbore (floor Z 17.5), 8 mm grip, M3 nut in a hex well
|
||||
from below (top of the well Z 9.5).
|
||||
- PCB: long axis along the body, 4 bosses ⌀5 × 1 mm under the top plate
|
||||
on the PCB hole grid (35.78 × 78.96), ⌀2.5 holes (self-tapping M3?),
|
||||
⌀5 × 1 mm recess on top. PCB top face at Z 26.
|
||||
- Teensy cut-out 23.78 × 66.96, centred.
|
||||
- Slide switch at the front: slot 8 × 16 (v 57..73), ⌀2 screw holes 19 mm
|
||||
apart, "ON"/"OFF" engraved 1 mm, readable from behind the robot.
|
||||
- Battery bay on the bottom plate: rails 3 × 13.5 mm high at |u| 26.06..29.06
|
||||
(52.1 mm between them), from v -74.21 (open, rear) to 31.79, end stop at
|
||||
v 28.79..31.79.
|
||||
|
||||
## Bugs in the original, fixed here
|
||||
|
||||
- Left-front pivot boss at u = -50.894 instead of -50.
|
||||
- Right-front corner has no coxa servo (only 5 of 6 placed).
|
||||
- Only 2 of 6 legs (the mid legs), plus mirror-naming leftovers and the
|
||||
empty `dontdeleteme` component (nothing to port).
|
||||
- Hex nut wells rotated randomly; now one orientation.
|
||||
|
||||
## Leg dimensions (for robot/Config.h and the Unity sim)
|
||||
|
||||
Measured on the original solids (left mid leg, `bottom-leg v31`):
|
||||
|
||||
| Item | Value | Config.h today |
|
||||
|------|-------|----------------|
|
||||
| Coxa: coxa axis to femur axis, horizontal | 55.0 mm | `COXA_LEN_MM` 55 (matches) |
|
||||
| Femur: femur axis to knee axis | 100.0 mm | `FEMUR_LEN_MM` 100 (matches) |
|
||||
| Tibia: knee axis to foot ball centre | 150.0 mm | `TIBIA_LEN_MM` 150 (matches) |
|
||||
| Foot ball radius | 5.0 mm + 0.5 mm TPU sock wall (same on the real robot) | not modelled |
|
||||
| Femur/knee axes height | in the coxa's joint plane, 13.5 mm above the bottom plate's top face | coxa pivot at Z 0 (matches if Z 0 = that plane) |
|
||||
| Leg mounts (coxa axes) | (±100, ±50), (0, ±65) | matches |
|
||||
| Original pose | femur 0°, tibia -90° (foot straight under the knee) | |
|
||||
|
||||
So the link lengths need no change. What the firmware and Unity don't know
|
||||
yet: the ground contact is the foot ball's surface (5 mm + sock from the
|
||||
point the IK places), and the body's underside (coxa bracket lower arm) is
|
||||
21.5 mm below the joint plane.
|
||||
|
||||
## Physical limits
|
||||
|
||||
Collision sweep of the model's real parts (2D, per joint, in 0.25° steps;
|
||||
cross-checked with full CGAL intersections of body and legs). Joint
|
||||
convention as `robot/Config.h`.
|
||||
|
||||
| Joint | Range | Stopped by |
|
||||
|-------|-------|-----------|
|
||||
| Coxa, mid legs 1/4 | -66.5° .. 66.5° | coxa bracket shell hits the plate waist |
|
||||
| Coxa, legs 0 (front-right) / 3 (rear-left) | -95.75° .. 66.75° (sideways mount: -35.75 .. 126.75) | same |
|
||||
| Coxa, legs 2 (rear-right) / 5 (front-left) | -66.75° .. 95.75° (sideways mount: -126.75 .. 35.75) | same |
|
||||
| Femur | -101.25° .. 101.25° | femur link hits the coxa bracket |
|
||||
| Tibia | -177° .. 12.75° | rod folds into the femur link / tibia shell hits it |
|
||||
| Servo travel | coxa/femur ±90°, tibia -180..0° | from the `SERVOS` pulse ranges |
|
||||
| Leg width at the joints | 33.7 mm to the horn side + 21.5 mm to the bearing side | neighbours touch at 55.2 mm |
|
||||
|
||||
Hexagon layout (owner's request): corner legs mounted 60° round from the mid
|
||||
legs (mount angles ±30 / ±90 / ±150); this needs the four corner coxa
|
||||
brackets turned 60° on their servo splines. In the firmware (`Config.h`):
|
||||
per-leg `COXA_MIN_DEG`/`COXA_MAX_DEG`, all ±60 now (the sideways layout
|
||||
needed -32..60 / -60..32 at the corners),
|
||||
femur ±80 and tibia -150..-20 unchanged (inside the physical range), and
|
||||
`LEG_CLEARANCE_MM` 56 (was 30). `sim_smoke` passes; turning on the spot
|
||||
runs at ~77°/s with legs at least 97 mm apart (sideways layout: ~29°/s).
|
||||
|
||||
## Unity simulation
|
||||
|
||||
`export_unity.py` renders the parts into
|
||||
`Hexapod_Simulation_Unity/Assets/HexapodSim/Resources/OpenScadModel/*.bytes`
|
||||
(rerun it after changing the SCAD files). `OpenScadModel.cs` hides the old
|
||||
scene's robot and builds a new rig with the same names at the exact
|
||||
Config.h geometry; `HexapodSimulation.useOpenScadModel` switches it off.
|
||||
`Editor/OpenScadModelSnapshot.cs` renders it in batch mode for checks.
|
||||
|
||||
## Remote (`remote.scad`)
|
||||
|
||||
Rebuilt from `../fusion360/remote/remote v40.f3d` the same way (slices
|
||||
match the original). Bugs fixed: the lid was off by (0.406, 0.21, -0.365)
|
||||
mm (sat in the ledge, posts overlapping the bosses); the display bezel's
|
||||
mesh facets became a clean window; the stray Body12 (an ON-OFF bracket
|
||||
line never cut in) is left out. The display is a Nextion NX4832T035
|
||||
(3.5" Basic); its board's M3 holes are the lid's 94.2 x 48.5 post grid.
|
||||
Reference parts in `remote.scad`: the joysticks are JH-D400X-R4 (4-axis,
|
||||
X/Y ±25°, twist ±45°, button; owner), mounted with the dust cover on the
|
||||
lid's top face; their M2.5 square is 32.5 mm, so the lid's screw holes moved
|
||||
from the original's 33 mm square to 32.5. The battery is an 18650 UPS/boost
|
||||
board (89 x 22 mm, SMD-holder version, 22.4 mm tall; owner), on the bottom's
|
||||
two d5 standoffs at (±27.9, -15) with the floor slots beside them for a
|
||||
strap; its hole spacing (55.8 mm here) is not confirmed. No collisions.
|
||||
Also from the owner: the bump on top of the middle post is a lanyard loop
|
||||
(the original's tunnel through it is modelled); the square hole is the on/off
|
||||
slide switch, mounted on top (same switch as the robot). Inside: an IMU on the
|
||||
21 x 9 mm block (assumed GY-521), a USB-C breakout on the side-port block, and
|
||||
a Teensy **4.0** (confirmed by the owner, although `platformio.ini` builds
|
||||
the remote for a 4.1); it has no mounting holes and is placed on the
|
||||
hole-less 8 x 24 mm block as a guess.
|
||||
Open: what the 6 mm x 10 mm holes in the bottom bosses hold (magnets?), and
|
||||
whether the upper two bosses are meant to be asymmetric (x -80.387 / +82.73).
|
||||
|
||||
## Notes for future designs
|
||||
|
||||
- Servo wires barely fit on the real robot: the six servo headers hang
|
||||
above the battery bay, and the front pair sits right over the bay's end
|
||||
stop (X 28.8..31.8, top at Z 13.5). Kept as-is for now; give the plugs and
|
||||
wires more room next time (e.g. a lower or shorter end stop, or a battery
|
||||
that leaves space under the headers).
|
||||
- The nRF24 under the switch is fine on the real robot.
|
||||
|
||||
## Open questions
|
||||
|
||||
1. Front = switch end (inferred from the ON/OFF text orientation)?
|
||||
2. PCB direction: `PCB_USB_FRONT` (Teensy USB, 7805 and LED at the front,
|
||||
nRF24 and battery JST at the rear) or reversed?
|
||||
3. Battery size. The six servo headers hang above the bay (|Y| 14-15 mm)
|
||||
and the JP1/JP4 or JP3/JP6 pair touches the end stop; servo plugs and
|
||||
the battery need to share that space.
|
||||
4. Leg geometry: in the original the femur servo sits further out than
|
||||
`COXA_LEN_MM` = 55 suggests. Check when porting the legs.
|
||||
@@ -0,0 +1,205 @@
|
||||
// 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();
|
||||
@@ -0,0 +1,109 @@
|
||||
"""Export the OpenSCAD robot as meshes for the Unity simulation.
|
||||
|
||||
python export_unity.py (from hardware/openscad, needs OpenSCAD)
|
||||
|
||||
Writes Hexapod_Simulation_Unity/Assets/HexapodSim/Resources/OpenScadModel/*.bytes,
|
||||
loaded by Assets/HexapodSim/OpenScadModel.cs. Every file is one rig segment
|
||||
in the robot frame (mm, +X forward, +Y left, +Z up), relative to the
|
||||
segment's pivot, in the rest pose that matches the rig's zero rotations
|
||||
(coxa 0, femur 0, tibia -90):
|
||||
body Z = 0 on the joint plane (13.5 mm above the bottom plate)
|
||||
leg_<side>_* left = as leg 4 (mount +90), right = its mirror (mount -90)
|
||||
|
||||
File format (little endian): b"HXM1", int32 groups, then per group
|
||||
float32 r, g, b, a, int32 triangles, triangles * 9 float32 (x, y, z).
|
||||
Triangles wind counter-clockwise seen from outside (right-handed).
|
||||
"""
|
||||
import os, re, struct, subprocess, sys, tempfile
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__)).replace("\\", "/")
|
||||
OUT = os.path.normpath(os.path.join(HERE, "..", "..", "Hexapod_Simulation_Unity", "Assets",
|
||||
"HexapodSim", "Resources", "OpenScadModel"))
|
||||
OPENSCAD = os.environ.get("OPENSCAD", r"C:/Program Files/OpenSCAD/openscad.exe")
|
||||
HEADER = f"use <{HERE}/body.scad>\nuse <{HERE}/leg.scad>\n"
|
||||
JOINT_PLANE_Z = 13.5
|
||||
|
||||
|
||||
def hexcol(h, a=1.0):
|
||||
h = h.lstrip("#")
|
||||
return [int(h[i:i + 2], 16) / 255 for i in (0, 2, 4)] + [a]
|
||||
|
||||
|
||||
# segment -> list of (colour, OpenSCAD geometry in the segment's frame)
|
||||
BODY = [
|
||||
("#3a3a3a", f"translate([0, 0, -{JOINT_PLANE_Z}]) fusion_to_robot() bottom_half();"),
|
||||
("#555555", f"translate([0, 0, -{JOINT_PLANE_Z}]) fusion_to_robot() top_half();"),
|
||||
("#2a2a2a", f"translate([0, 0, -{JOINT_PLANE_Z}]) fusion_to_robot() body_servos_uv();"),
|
||||
("#2b6b3a", f"translate([0, 0, -{JOINT_PLANE_Z}]) pcb_in_body();"),
|
||||
]
|
||||
LEFT = "rotate([0, 0, 90]) " # leg frame -> robot frame for the left legs (mount +90)
|
||||
LEG = {
|
||||
"coxa": [
|
||||
("#d9822b", LEFT + "{ coxa_bracket_upper(); coxa_bracket_lower(); }"),
|
||||
("#bdbdbd", LEFT + "translate([55, 0, 0]) along_y(28.7) horn_adapter();"),
|
||||
("#5a3a1e", LEFT + "coxa_cables();"),
|
||||
],
|
||||
"femur": [
|
||||
("#4a7fbf", LEFT + "{ link_channel(); link_lid(); }"),
|
||||
("#2a2a2a", LEFT + "link_servos();"),
|
||||
("#5a3a1e", LEFT + "femur_cables();"),
|
||||
],
|
||||
"tibia": [
|
||||
("#5fa35f", LEFT + "{ tibia_bearing_half(); tibia_horn_half(); }"),
|
||||
("#bdbdbd", LEFT + "along_y(28.7) horn_adapter();"),
|
||||
("#1c1c1c", LEFT + "foot_sock();"),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def render(geometry, tmp):
|
||||
scad = os.path.join(tmp, "part.scad"); stl = os.path.join(tmp, "part.stl")
|
||||
with open(scad, "w") as f:
|
||||
f.write(HEADER + geometry + "\n")
|
||||
r = subprocess.run([OPENSCAD, "-o", stl, scad], capture_output=True, text=True)
|
||||
if r.returncode != 0 or not os.path.exists(stl):
|
||||
sys.exit(f"OpenSCAD failed on: {geometry}\n{r.stderr}")
|
||||
tris = []
|
||||
num = r"([-+0-9.eE]+)"
|
||||
for m in re.finditer(r"outer loop\s+vertex\s+%s\s+%s\s+%s\s+vertex\s+%s\s+%s\s+%s\s+vertex\s+%s\s+%s\s+%s" % ((num,) * 9),
|
||||
open(stl).read()):
|
||||
tris.append([float(v) for v in m.groups()])
|
||||
os.remove(stl)
|
||||
return tris
|
||||
|
||||
|
||||
def mirror_y(tris):
|
||||
# (x, y, z) -> (x, -y, z) and swap two vertices to keep outward winding
|
||||
out = []
|
||||
for t in tris:
|
||||
a, b, c = t[0:3], t[3:6], t[6:9]
|
||||
a, b, c = [a[0], -a[1], a[2]], [b[0], -b[1], b[2]], [c[0], -c[1], c[2]]
|
||||
out.append(a + c + b)
|
||||
return out
|
||||
|
||||
|
||||
def write(name, groups):
|
||||
path = os.path.join(OUT, name + ".bytes")
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"HXM1" + struct.pack("<i", len(groups)))
|
||||
for col, tris in groups:
|
||||
f.write(struct.pack("<4f", *col) + struct.pack("<i", len(tris)))
|
||||
for t in tris:
|
||||
f.write(struct.pack("<9f", *t))
|
||||
print(f" {name}: {sum(len(t) for _, t in groups)} triangles")
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
print("body")
|
||||
write("body", [(hexcol(c), render(g, tmp)) for c, g in BODY])
|
||||
for seg, parts in LEG.items():
|
||||
print("leg", seg)
|
||||
groups = [(hexcol(c), render(g, tmp)) for c, g in parts]
|
||||
write(f"leg_left_{seg}", groups)
|
||||
write(f"leg_right_{seg}", [(c, mirror_y(t)) for c, t in groups])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,383 @@
|
||||
// 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();
|
||||
Binary file not shown.
@@ -0,0 +1,245 @@
|
||||
// Handheld remote: bottom shell + lid, rebuilt from the Fusion 360 model
|
||||
// fusion360/remote/remote v40.f3d (the `bottom` and `top` solids in
|
||||
// "remote v40.stp"; remote v4 is an early flat stick plate).
|
||||
//
|
||||
// Written in the original's top-view coordinates (u, v) = (Fusion x,
|
||||
// -Fusion z), so every number can be checked against the Fusion file.
|
||||
// Z = 0 on the rim of the bottom shell (the lid's top face sits flush).
|
||||
// Text on the lid reads upright with +v up.
|
||||
//
|
||||
// Deviations from the original (its bugs):
|
||||
// - Lid moved by (-0.406, -0.21, +0.365) mm: it was off-centre and sat
|
||||
// 0.365 mm into the bottom's ledge, its posts overlapping the bosses.
|
||||
// - The display bezel's stepped facets (an imported mesh) are a clean
|
||||
// rectangular window plus its 0.45 mm lip; small clip notches omitted.
|
||||
// - Body12 (an "ON-OFF" bracket line that was never cut into the lid) left out.
|
||||
|
||||
/* [Shell] */
|
||||
CORNERS = [[-66.5, -58.883], [66.5, -58.883], [66.5, 41.117], [-66.5, 41.117]];
|
||||
R_OUT = 37.5; // outside of the bottom shell
|
||||
R_WALL = 32.5; // inside of the walls (5 mm)
|
||||
R_LEDGE = 35.5; // rebate the lid drops into
|
||||
R_LID = 35.0; // lid outline (0.5 mm clearance in the rebate)
|
||||
FLOOR_Z = [-45, -42];
|
||||
LEDGE_Z = -5;
|
||||
LID_Z = [-5, 0];
|
||||
|
||||
/* [Fixing] */
|
||||
BOSSES = [[-30, 40], [30, 40], [-80.387, -72.711], [82.73, -73]];
|
||||
BOSS_D = 10;
|
||||
BOSS_TOP = -35;
|
||||
BOSS_HOLE_D = 6; // through the floor, 10 mm deep (magnets?)
|
||||
POST_HOLE_D = 3; // blind, from the lid post's end
|
||||
|
||||
/* [Sticks, switch, display] */
|
||||
STICKS = [[-66.5, 41.117], [66.5, 41.117]];
|
||||
STICK_HOLE_D = 40;
|
||||
STICK_SCREW = 16.25; // 4 x d2 (M2.5 self-tapping) on the joystick's 32.5 mm square (original: 33)
|
||||
SWITCH_SLOT = [[-4, 33.975], [4, 49.975]]; // on/off slide switch, mounted on top (same as the robot's)
|
||||
SWITCH_HOLES = [[0, 32.475], [0, 51.475]];
|
||||
PILL = [[0, 5], [0, 15]]; // centres of the round ends
|
||||
WINDOW = [[-37.94, -66.964], [37.96, -16.064]];
|
||||
WINDOW_LIP = [0.45, -1.05, -0.45]; // inset, z range
|
||||
DISPLAY_POCKET = [[-49.0, -73.0], [52.258, -10.0]];
|
||||
DISPLAY_POCKET_Z = -2; // pocket from the lid's underside up to here
|
||||
DISPLAY_POSTS = [[-44.939, -65.772], [49.261, -65.772], [-44.939, -17.272], [49.261, -17.272]];
|
||||
TEXT = [["ON", [0.7, 26.29]], ["OFF", [0.15, 57.61]]];
|
||||
ENGRAVE_D = 1;
|
||||
|
||||
/* [Rendering] */
|
||||
SHOW_BOTTOM = true;
|
||||
SHOW_LID = true;
|
||||
SHOW_DISPLAY = true;
|
||||
SHOW_STICKS = true;
|
||||
SHOW_BATTERY = true;
|
||||
EXPLODE = 0;
|
||||
|
||||
/* [Hidden] */
|
||||
$fa = 2; $fs = 0.3;
|
||||
BIG = 300;
|
||||
|
||||
module lx(h) linear_extrude(h, convexity = 10) children();
|
||||
|
||||
module rounded_2d(r) hull() for (c = CORNERS) translate(c) circle(r = r);
|
||||
|
||||
module stadium_2d(a, b, r) hull() { translate(a) circle(r = r); translate(b) circle(r = r); }
|
||||
|
||||
// ---------- bottom shell --------------------------------------------------
|
||||
|
||||
module remote_bottom() render(convexity = 10) difference() {
|
||||
union() {
|
||||
difference() {
|
||||
translate([0, 0, FLOOR_Z[0]]) lx(-FLOOR_Z[0]) rounded_2d(R_OUT);
|
||||
translate([0, 0, FLOOR_Z[1]]) lx(BIG) rounded_2d(R_WALL);
|
||||
translate([0, 0, LEDGE_Z]) lx(BIG) rounded_2d(R_LEDGE);
|
||||
}
|
||||
for (b = BOSSES) translate([b[0], b[1], FLOOR_Z[1] - 1]) cylinder(d = BOSS_D, h = BOSS_TOP - FLOOR_Z[1] + 1);
|
||||
// blocks rising from the floor
|
||||
translate([-10.5, 29, FLOOR_Z[1] - 1]) cube([21, 9, -17 - FLOOR_Z[1] + 1]);
|
||||
translate([-4, 44, FLOOR_Z[1] - 1]) cube([8, 24, -17 - FLOOR_Z[1] + 1]);
|
||||
for (m = [-1, 1]) translate([m * 27.9, -15, FLOOR_Z[1] - 1]) cylinder(d = 5, h = 4);
|
||||
translate([88, -10, FLOOR_Z[1] - 1]) cube([12, 14, -28 - FLOOR_Z[1] + 1]); // port board block
|
||||
// post up through the lid's slot, with a rounded lanyard loop on top
|
||||
translate([0, 0, FLOOR_Z[1] - 1]) lx(-FLOOR_Z[1] + 1) stadium_2d(PILL[0], PILL[1], 4.75);
|
||||
difference() {
|
||||
intersection() {
|
||||
hull() for (p = PILL) translate([p[0], p[1], 2.75]) sphere(r = 4.75);
|
||||
translate([-10, 0, 0]) cube([20, 30, 10]);
|
||||
}
|
||||
hull() for (p = PILL) translate([-10, p[1], 2.75]) rotate([0, 90, 0]) cylinder(r = 2.75, h = 20);
|
||||
}
|
||||
}
|
||||
for (b = BOSSES) translate([b[0], b[1], FLOOR_Z[0] - 1]) cylinder(d = BOSS_HOLE_D, h = BOSS_TOP - FLOOR_Z[0] + 2);
|
||||
for (m = [-1, 1]) translate([m * 6.562, 35, FLOOR_Z[1]]) cylinder(d = 3, h = 30);
|
||||
for (m = [-1, 1]) translate([m * 27.9, -15, FLOOR_Z[1]]) cylinder(d = 3, h = 10);
|
||||
for (m = [-1, 1]) translate([0, 0, FLOOR_Z[0] - 1])
|
||||
lx(5) stadium_2d([m * 23.9, -17], [m * 23.9, -13], 1);
|
||||
translate([98, -10, -35]) cube([10, 14, 15]); // side port opening
|
||||
for (v = [-7.5, 1.5]) translate([91, v, -37]) cylinder(d = 3, h = 10);
|
||||
}
|
||||
|
||||
// ---------- lid -----------------------------------------------------------
|
||||
|
||||
module lid_plate_2d() difference() {
|
||||
rounded_2d(R_LID);
|
||||
for (s = STICKS) translate(s) {
|
||||
circle(d = STICK_HOLE_D);
|
||||
for (du = [-1, 1], dv = [-1, 1]) translate([du, dv] * STICK_SCREW) circle(d = 2);
|
||||
}
|
||||
stadium_2d(PILL[0], PILL[1], 5);
|
||||
translate(SWITCH_SLOT[0]) square(SWITCH_SLOT[1] - SWITCH_SLOT[0]);
|
||||
for (h = SWITCH_HOLES) translate(h) circle(d = 2);
|
||||
translate(WINDOW[0]) offset(r = 0.5) offset(delta = -0.5) square(WINDOW[1] - WINDOW[0]);
|
||||
}
|
||||
|
||||
module remote_lid() render(convexity = 10) difference() {
|
||||
union() {
|
||||
translate([0, 0, LID_Z[0]]) lx(LID_Z[1] - LID_Z[0]) lid_plate_2d();
|
||||
for (b = BOSSES) translate([b[0], b[1], BOSS_TOP]) cylinder(d = BOSS_D, h = LID_Z[0] - BOSS_TOP + 1);
|
||||
// window lip, left over from the original's display bezel
|
||||
translate([0, 0, WINDOW_LIP[1]]) lx(WINDOW_LIP[2] - WINDOW_LIP[1]) difference() {
|
||||
translate(WINDOW[0]) square(WINDOW[1] - WINDOW[0]);
|
||||
translate(WINDOW[0] + [1, 1] * WINDOW_LIP[0]) square(WINDOW[1] - WINDOW[0] - [2, 2] * WINDOW_LIP[0]);
|
||||
}
|
||||
}
|
||||
translate([0, 0, LID_Z[0] - 1]) lx(DISPLAY_POCKET_Z - LID_Z[0] + 1) difference() {
|
||||
translate(DISPLAY_POCKET[0]) square(DISPLAY_POCKET[1] - DISPLAY_POCKET[0]);
|
||||
for (p = DISPLAY_POSTS) translate(p) circle(d = 6);
|
||||
}
|
||||
for (p = DISPLAY_POSTS) translate([p[0], p[1], LID_Z[0] - 1]) cylinder(d = 2.5, h = 4);
|
||||
for (b = BOSSES) translate([b[0], b[1], BOSS_TOP - 1]) cylinder(d = POST_HOLE_D, h = LID_Z[0] - BOSS_TOP + 1);
|
||||
for (t = TEXT) translate([t[1][0], t[1][1], LID_Z[1] - ENGRAVE_D]) lx(ENGRAVE_D + 1)
|
||||
text(t[0], size = 5.8, halign = "center", valign = "center", font = "Liberation Sans");
|
||||
}
|
||||
|
||||
// ---------- reference parts -----------------------------------------------
|
||||
|
||||
// Nextion NX4832T035 (3.5" Basic): 100.5 x 54.94 mm board, M3 holes 3.15 /
|
||||
// 3.22 mm from its edges = the lid's 94.2 x 48.5 post grid; LCD module
|
||||
// approximate, its visible area (73.44 x 48.96) under the window.
|
||||
module nextion_3_5() {
|
||||
centre = (DISPLAY_POSTS[0] + DISPLAY_POSTS[3]) / 2;
|
||||
translate([centre[0], centre[1], DISPLAY_POCKET_Z - 4.5 - 1.6]) {
|
||||
color("#1e5a2a") difference() {
|
||||
translate([-100.5/2, -54.94/2, 0]) cube([100.5, 54.94, 1.6]);
|
||||
for (p = DISPLAY_POSTS) translate([p[0] - centre[0], p[1] - centre[1], -1]) cylinder(d = 3.2, h = 4);
|
||||
}
|
||||
}
|
||||
win = (WINDOW[0] + WINDOW[1]) / 2;
|
||||
color("#202020") translate([win[0] - 84/2, win[1] - 54/2, DISPLAY_POCKET_Z - 4.5]) cube([84, 54, 4.5]);
|
||||
color("#3050a0") translate([win[0] - 73.44/2, win[1] - 48.96/2, DISPLAY_POCKET_Z - 0.2]) cube([73.44, 48.96, 0.2]);
|
||||
}
|
||||
|
||||
// JH-D400X-R4 4-axis joystick (X/Y +-25 deg, twist +-45 deg, button),
|
||||
// from its dimension drawing. Mounted with its dust cover on the lid's top
|
||||
// face; the body hangs through the d40 opening. Local frame: stick axis on
|
||||
// Z, Z = 0 on the dust cover's underside. Pots on +X and +Y.
|
||||
JOY_COVER_D = 53.5;
|
||||
JOY_COVER_T = 3; // not dimensioned; drawing proportions
|
||||
JOY_BOX = 40.4; // control box, square
|
||||
JOY_BOX_H = 25.5;
|
||||
JOY_BELOW = 42; // box bottom to dust cover top
|
||||
JOY_ABOVE = 52.5; // dust cover top to button top
|
||||
JOY_POT_D = 16.5;
|
||||
JOY_POT_L = (49.6 - 40.4);
|
||||
|
||||
module jh_d400x() {
|
||||
top = JOY_COVER_T;
|
||||
color("#202020") {
|
||||
difference() {
|
||||
cylinder(d = JOY_COVER_D, h = JOY_COVER_T);
|
||||
for (du = [-1, 1], dv = [-1, 1]) translate([du * 16.25, dv * 16.25, -1]) cylinder(d = 2.7, h = 5);
|
||||
}
|
||||
translate([0, 0, top]) cylinder(d1 = 39.5, d2 = 30, h = 3); // dust ring / boot
|
||||
translate([0, 0, top - JOY_BELOW]) translate([-JOY_BOX/2, -JOY_BOX/2, 0]) cube([JOY_BOX, JOY_BOX, JOY_BOX_H]);
|
||||
translate([0, 0, top - JOY_BELOW + JOY_BOX_H]) cylinder(d = 34, h = JOY_BELOW - JOY_BOX_H - JOY_COVER_T + 0.01);
|
||||
for (a = [0, 90]) rotate([0, 0, a])
|
||||
translate([JOY_BOX/2, 0, top - JOY_BELOW + JOY_BOX_H/2]) rotate([0, 90, 0]) cylinder(d = JOY_POT_D, h = JOY_POT_L);
|
||||
// handle: neck, knurled body, top with the button
|
||||
translate([0, 0, top + 6]) cylinder(d = 6, h = 12);
|
||||
translate([0, 0, top + 16]) cylinder(d = 22, h = 8);
|
||||
translate([0, 0, top + 24]) cylinder(d = 35, h = 22);
|
||||
translate([0, 0, top + 46]) cylinder(d1 = 35, d2 = 26, h = 5);
|
||||
}
|
||||
color("#c0c0c0") translate([0, 0, top + 51]) cylinder(d = 8, h = JOY_ABOVE - 51);
|
||||
}
|
||||
|
||||
// 18650 UPS / boost board (5/9/12 V, Type-C charging), "SMD battery box"
|
||||
// version: 89 x 22 mm board, holder on top, 22.4 mm overall, d3 holes.
|
||||
// Placed on the bottom's two d5 standoffs at (+-27.9, -15); the board's real
|
||||
// hole spacing isn't confirmed. The floor slots beside them take a strap.
|
||||
UPS_CENTRE = [0, -15];
|
||||
UPS_Z = -39; // standoff tops
|
||||
module ups_18650() translate([UPS_CENTRE[0], UPS_CENTRE[1], UPS_Z]) {
|
||||
color("#202020") difference() {
|
||||
translate([-89/2, -22/2, 0]) cube([89, 22, 1.6]);
|
||||
for (m = [-1, 1]) translate([m * 27.9, 0, -1]) cylinder(d = 3, h = 4);
|
||||
}
|
||||
color("#151515") translate([-77/2, -21/2, 1.6]) difference() {
|
||||
cube([77, 21, 22.4 - 1.6]);
|
||||
translate([3, 1.5, 3]) cube([71, 18, 30]);
|
||||
}
|
||||
color("#4060c0") translate([-65/2, 0, 1.6 + 3 + 18.5/2]) rotate([0, 90, 0]) cylinder(d = 18.5, h = 65);
|
||||
color("#b0b0b0") translate([89/2 - 7, -4.5, 1.6]) cube([7.5, 9, 3.2]); // Type-C
|
||||
}
|
||||
|
||||
// Small USB-C breakout (owner: glued in the holder at the side port), on the
|
||||
// port block (u 88..99, v -10..4, top Z -28) with the receptacle facing the
|
||||
// opening (v -10..4, Z -35..-20). Board size approximate.
|
||||
module usb_c_breakout() translate([88, -10, -28]) {
|
||||
color("#1a3a8a") cube([11, 14, 1.6]);
|
||||
color("#c0c0c0") translate([11 - 7.3, 7 - 8.9/2, 1.6]) cube([7.5, 8.9, 3.2]);
|
||||
}
|
||||
|
||||
// IMU (owner: on the big standoff in the middle), assumed a GY-521 / MPU6050
|
||||
// like the robot's: 21.2 x 15.6 mm board on the 21 x 9 mm block (top Z -17),
|
||||
// its hole end over the block's two d3 holes at v 35, board towards the centre.
|
||||
module imu_gy521() translate([0, 35 + 3.5 - 15.6, -17]) {
|
||||
color("#1f4fa0") translate([-21.2/2, 0, 0]) cube([21.2, 15.6, 1.6]);
|
||||
color("#222") translate([-2, 7, 1.6]) cube([4, 4, 1]);
|
||||
}
|
||||
|
||||
// Teensy 4.0 (owner; no mounting holes), assumed on the hole-less 8 x 24 mm
|
||||
// block (u -4..4, v 44..68, top Z -17). Across the block by default; lengthwise
|
||||
// it would end 0.1 mm from the shell wall.
|
||||
TEENSY_LENGTHWISE = false;
|
||||
module teensy_4_0() translate([0, 56, -17]) rotate([0, 0, TEENSY_LENGTHWISE ? 90 : 0]) {
|
||||
color("#1e5a2a") translate([-35.6/2, -17.8/2, 0]) cube([35.6, 17.8, 1.6]);
|
||||
color("#222") translate([-3, -5, 1.6]) cube([10, 10, 1.2]); // i.MX RT1062
|
||||
color("#c0c0c0") translate([-35.6/2 - 1, -7.5/2, 1.6]) cube([5.7, 7.5, 2.9]); // micro-USB
|
||||
}
|
||||
|
||||
module remote_assembly() {
|
||||
if (SHOW_BOTTOM) color("#3a3a3a") remote_bottom();
|
||||
if (SHOW_LID) translate([0, 0, EXPLODE]) color("#5a5a5a") remote_lid();
|
||||
if (SHOW_DISPLAY) translate([0, 0, EXPLODE]) nextion_3_5();
|
||||
if (SHOW_BATTERY) ups_18650();
|
||||
if (SHOW_BATTERY) usb_c_breakout();
|
||||
if (SHOW_BATTERY) imu_gy521();
|
||||
if (SHOW_BATTERY) teensy_4_0();
|
||||
// pots towards the nearest side wall and the near edge, where there's room
|
||||
if (SHOW_STICKS) for (s = STICKS) translate([s[0], s[1], LID_Z[1] + EXPLODE])
|
||||
mirror([s[0] < 0 ? 1 : 0, 0, 0]) jh_d400x();
|
||||
}
|
||||
|
||||
remote_assembly();
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,36 @@
|
||||
// The whole 2026 hexapod: body (body.scad) plus six legs (leg.scad),
|
||||
// placed and posed like robot/Config.h. Robot frame: +X forward (switch
|
||||
// end), +Y left, +Z up, Z = 0 on the bottom plate's top face.
|
||||
|
||||
use <body.scad>
|
||||
use <leg.scad>
|
||||
|
||||
/* [Pose] */
|
||||
// "original" = the Fusion model's pose; "stand" = firmware stance
|
||||
// (Kinematics::neutralStance(): feet 130 mm out, 150 mm down).
|
||||
POSE_NAME = "stand"; // [original, stand]
|
||||
// Optional per-leg override, [coxa, femur, tibia] in degrees, legs 0..5.
|
||||
POSE_OVERRIDE = [];
|
||||
|
||||
// Leg mounts from robot/Config.h: x, y, mount angle (0..2 right, 3..5 left).
|
||||
// Hexagon layout: corner legs 60 deg round from the mid legs.
|
||||
LEG_MOUNT = [[100, -50, -30], [0, -65, -90], [-100, -50, -150],
|
||||
[-100, 50, 150], [0, 65, 90], [100, 50, 30]];
|
||||
JOINT_PLANE_Z = 13.5; // femur/knee axes, halfway up the body cavity
|
||||
|
||||
POSES = [["original", [0, 0, -90]], ["stand", [0, -1.2, -98.3]]];
|
||||
function base_pose() = [for (p = POSES) if (p[0] == POSE_NAME) p[1]][0];
|
||||
function leg_pose(i) = len(POSE_OVERRIDE) == 6 ? POSE_OVERRIDE[i] : base_pose();
|
||||
|
||||
module robot() {
|
||||
body_assembly();
|
||||
for (i = [0:5]) {
|
||||
m = LEG_MOUNT[i];
|
||||
p = leg_pose(i);
|
||||
translate([m[0], m[1], JOINT_PLANE_Z])
|
||||
if (m[2] > 0) rotate([0, 0, m[2]]) leg(p);
|
||||
else mirror([0, 1, 0]) rotate([0, 0, -m[2]]) leg([-p[0], p[1], p[2]]);
|
||||
}
|
||||
}
|
||||
|
||||
robot();
|
||||
@@ -0,0 +1,28 @@
|
||||
// Hitec HS-485HB, from the servo's Fusion model (servo-hitec-hs-485hb).
|
||||
// Local frame: shaft on the Z axis, case extending +X, Z = 0 at the ear
|
||||
// underside. Include (not use) this file to get the constants.
|
||||
|
||||
SERVO_CASE = [[-10, -10, -28], [30, 10, 9.75]];
|
||||
SERVO_EARS = [[-16, -10, 0], [36, 10, 3]];
|
||||
SERVO_EAR_HOLES = [[-13.9, 5], [-13.9, -5], [34.35, 5], [34.35, -5]];
|
||||
SERVO_EAR_SCREW_D = 2.5; // pilot hole for the ear screws in printed parts
|
||||
POCKET_CLEAR = 0.5;
|
||||
|
||||
module servo_pocket_2d() {
|
||||
translate([SERVO_CASE[0][0] - POCKET_CLEAR, SERVO_CASE[0][1] - POCKET_CLEAR])
|
||||
square([SERVO_CASE[1][0] - SERVO_CASE[0][0] + 2*POCKET_CLEAR,
|
||||
SERVO_CASE[1][1] - SERVO_CASE[0][1] + 2*POCKET_CLEAR]);
|
||||
for (h = SERVO_EAR_HOLES) translate(h) circle(d = SERVO_EAR_SCREW_D);
|
||||
}
|
||||
|
||||
module hs485hb() {
|
||||
color("#303030") {
|
||||
translate(SERVO_CASE[0]) cube(SERVO_CASE[1] - SERVO_CASE[0]);
|
||||
difference() {
|
||||
translate(SERVO_EARS[0]) cube(SERVO_EARS[1] - SERVO_EARS[0]);
|
||||
for (h = SERVO_EAR_HOLES) translate([h[0], h[1], -1]) cylinder(d = 4.42, h = 5);
|
||||
}
|
||||
translate([0, 0, 9.75]) cylinder(d = 14, h = 2.25);
|
||||
}
|
||||
color("#e0e0e0") translate([0, 0, 12]) cylinder(d = 5, h = 3.25);
|
||||
}
|
||||
Reference in new issue
Block a user