Read this model as text
A printable wheeled bipedal balancer with two straight legs. Six bus servos: per leg a roll joint on the body, a pitch joint below it, and a wheel drive. The body is a two-shell chassis with bays for the electronics, the legs are trussed plates, and each wheel is a rigid spoked hub with a separate TPU tyre that clicks onto it.
The image shows the solid OpenSCAD renders at part assembly · hip_actuator STS3250 · wheel_actuator STS3215 · leg_length 140 · wheel_diameter 140 · wheel_width 26 · body_length 150 · body_width 80 · body_height 70 · mass_g 1200 · leg_pitch 0 · leg_roll 0 · sensor lidar.
Wheeled bipedal balancer
mick/wheeled-biped-balancer@0.3.0
Report
For a legal or policy problem: a license violation, personal data, somebody else’s work republished as their own. A model that prints badly is a comment, not a report.
Parameters
from sourceDescription
What is this
A printable wheeled bipedal balancer with two straight legs. Six bus servos: per leg a roll joint on the body, a pitch joint below it, and a wheel drive. The body is a two-shell chassis with bays for the electronics, the legs are trussed plates, and each wheel is a rigid spoked hub with a separate TPU tyre that clicks onto it.
`part` switches between the full assembly and each printable part laid out in its print orientation: body_tray, body_lid, hip_bracket, leg, wheel_hub, tire. Print two of everything except the body.
What you use it for
Building the balancer, and sizing it first. Pose the legs with leg_pitch and leg_roll; the console reports stance height, track width, the static torque on each hip joint against the servo's rated torque, and whether each part fits a 180 mm bed.
The electronics bay in the tray is laid out front to back: Raspberry Pi Zero 2 W on four standoffs at the front, a bus servo driver board on two standoffs in the middle, and two 18650 cells in printed cradles at the rear. IMU standoffs hang from the underside of the lid at the body centre. A slot in the rear wall takes a rocker switch or charge lead.
Design notes
Every servo horn faces outboard. The roll servo bolts to the outside of the tray wall from inside, shaft pointing forward. An L-shaped hip bracket bolts to that horn: a vertical plate on the horn and a horizontal shelf below the servo, and the pitch servo bolts under the shelf with its shaft pointing out. The leg plate bolts to the pitch horn. The wheel servo bolts to the inboard face of the leg at the bottom, shaft through the plate, and the hub bolts to its horn. Every screw is reachable from outside with the assembly together.
Fasteners: four M3 heat-set inserts in the tray corners and four M3 screws through the lid. Everything on the servos uses the M2 screws that come with them. The tyre has no fastener: an internal ridge snaps into a groove around the hub, and the TPU stretches on.
Three things to measure before printing. The servo case dimensions and the shaft offset along the case are from Feetech's drawings and should be checked against your units. The side and top mounting hole positions are set as constants in the hidden section and marked as such; measure yours. The horn bolt circle is set to 14 mm on four holes; measure the horn that ships with your servos.
The tray's side walls carry the roll servos, so the lid split sits above the servo top. Rear and front side vents double as lightening. The lid top is chamfered in and carries a ring boss with a four-hole pattern for a small 2D lidar at the front, and a window in the front face for a camera.
Parameters
part picks what to render. hip_actuator and wheel_actuator pick the servo. leg_length, wheel_diameter and wheel_width size the leg and wheel. body_length, body_width and body_height size the chassis, and mass_g feeds the torque check. leg_pitch and leg_roll pose the legs in the assembly view. sensor picks none, lidar, camera or both.
Use with your agent
{
"mcpServers": {
"remixcad": {
"type": "http",
"url": "https://remix-cad.vercel.app/api/mcp"
}
}
}get_model("mick/wheeled-biped-balancer@0.3.0")Returns the manifest, the source, the import closure and the license obligations. Full MCP setup.
Parameter table
Parsed from the Customizer annotations in main.scad| Name | Default | Range | Unit | Description |
|---|---|---|---|---|
| part | "assembly" | assembly, body_tray, body_lid, hip_bracket, leg, wheel_hub, tire | ||
| hip_actuator | "STS3250" | STS3215, STS3250 | ||
| wheel_actuator | "STS3215" | STS3215, STS3250 | ||
| leg_length | 140 | 60 to 260 step 1 | ||
| wheel_diameter | 140 | 60 to 240 step 5 | ||
| wheel_width | 26 | 14 to 60 step 1 | ||
| body_length | 150 | 120 to 200 step 1 | ||
| body_width | 80 | 70 to 140 step 1 | ||
| body_height | 70 | 56 to 120 step 1 | ||
| mass_g | 1200 | 400 to 3000 step 50 | ||
| leg_pitch | 0 | -60 to 60 step 1 | ||
| leg_roll | 0 | -30 to 45 step 1 | ||
| sensor | "lidar" | none, lidar, camera, both |
Lineage
0 models depend on this oneOnly what this model came from and what came from it.
License
Whoever downloads this takes these obligations along- mick/wheeled-biped-balancer@0.3.0 is MIT
Source
main.scad is the entrypointmain.scad
// Wheeled bipedal balancer, printable parts.
// Two straight legs, each with a roll joint on the body, a pitch joint below
// it and a driven wheel. Two-shell chassis with electronics bays. Separate
// rigid hub and TPU tyre per wheel.
/* [Part] */
// What to render: the assembly, or one printable part in print orientation.
part = "assembly"; // [assembly, body_tray, body_lid, hip_bracket, leg, wheel_hub, tire]
/* [Actuators] */
// Hip actuator, used for both the roll and the pitch joint.
hip_actuator = "STS3250"; // [STS3215, STS3250]
// Wheel drive actuator, one per wheel.
wheel_actuator = "STS3215"; // [STS3215, STS3250]
/* [Leg] */
// Pitch pivot to wheel axle.
leg_length = 140; // [60:1:260]
// Diameter measured over the tyre tread.
wheel_diameter = 140; // [60:5:240]
// Width across the tyre.
wheel_width = 26; // [14:1:60]
/* [Body] */
// Fore-aft length of the chassis.
body_length = 150; // [120:1:200]
// Width of the chassis.
body_width = 80; // [70:1:140]
// Height of the chassis, tray plus lid.
body_height = 70; // [56:1:120]
// All-up mass for the torque check. Changes no geometry.
mass_g = 1200; // [400:50:3000]
/* [Pose] */
// Leg swung forward from vertical, degrees. Negative is back.
leg_pitch = 0; // [-60:1:60]
// Leg swung outward from vertical, degrees. Negative is in.
leg_roll = 0; // [-30:1:45]
/* [Sensor] */
// What sits on the body.
sensor = "lidar"; // [none, lidar, camera, both]
/* [Hidden] */
$fn = 36;
wall = 2.4;
corner_r = 8;
corner_in = 9; // screw post inset from the corner
split_z = 46; // tray height; lid takes the rest
hip_z = 34; // roll axis above the tray floor
insert_d = 4.0; // M3 heat-set insert hole
m3_clear = 3.4;
m3_head = 6.4;
m2_hole = 2.2;
horn_circle = 14; // horn bolt circle, 4 holes -- measure your horn
horn_hole = 2.2;
horn_thick = 6; // servo face to mating face, horn included
shaft_off = 10; // shaft centre from the near end of the case -- check
side_hole_in = 6; // side mounting holes inset from case ends/faces -- measure
face_hx = 10; // top-face mounting holes around the shaft -- measure
face_hz = 8;
lt = 6; // leg plate thickness
pt = 3; // bracket plate thickness
tire_t = 12; // tyre radial thickness
tread_n = 28;
// Servo case [L, W, H]. Shaft on the L x W face, shaft_off from one L end.
function act_size(n) = n == "STS3250" ? [45.2, 24.7, 35.0] : [44.8, 24.0, 35.5];
function act_stall(n) = n == "STS3250" ? 50 : 30;
hip_size = act_size(hip_actuator);
whl_size = act_size(wheel_actuator);
hip_stall = act_stall(hip_actuator);
whl_stall = act_stall(wheel_actuator);
hip_rated = hip_stall * 0.25;
rL = hip_size[0]; rW = hip_size[1]; rH = hip_size[2];
wheel_r = wheel_diameter / 2;
mass_kg = mass_g / 1000;
lid_h = body_height - split_z;
// Right-leg frame, origin on the roll axis. Left leg is a mirror.
roll_y = body_width / 2 + rW / 2;
shelf_top = -(rL - shaft_off) - 1;
pitch_face_y = -rW / 2 + rH;
pitch_z = shelf_top - pt - 0.5 - rW / 2;
leg_y_in = pitch_face_y + horn_thick;
wheel_yl = leg_y_in + lt + horn_thick + wheel_width / 2;
// Wheel centre in world coordinates, right leg.
zz = pitch_z - leg_length * cos(leg_pitch);
wy_r = wheel_yl * cos(leg_roll) - zz * sin(leg_roll);
wz_r = wheel_yl * sin(leg_roll) + zz * cos(leg_roll);
wc_y = roll_y + wy_r;
wc_z = hip_z + wz_r;
ground_z = wc_z - wheel_r * cos(leg_roll);
stance_h = -ground_z;
track_w = 2 * wc_y + wheel_width * cos(leg_roll);
sensor_h = (sensor == "lidar" || sensor == "both") ? 36 : 0;
total_h = stance_h + body_height + sensor_h;
pitch_need = abs(mass_kg / 2 * leg_length * sin(leg_pitch) / 10);
roll_need = abs(mass_kg / 2 * wy_r / 10);
roll_stand = mass_kg / 2 * wheel_yl / 10;
arm_lim = hip_rated / (mass_kg / 2);
pitch_max = arm_lim >= leg_length / 10 ? 90 : asin(arm_lim / (leg_length / 10));
rim_force = whl_stall / (wheel_r / 10);
leg_print = leg_length + 34;
// Electronics bay positions along x, front is +x.
pi_x = body_length / 2 - wall - 4 - 32.5;
bat_x = -body_length / 2 + wall + 4 + 20.5;
drv_x = ((pi_x - 32.5) + (bat_x + 20.5)) / 2;
lidar_x = body_length / 4;
echo(str("stance height, tray floor to ground: ", round(stance_h), " mm; overall ", round(total_h), " mm; track ", round(track_w), " mm"));
echo(str("pitch joint: ", round(pitch_need*10)/10, " kg.cm now, holds up to ", round(pitch_max), " deg on ", hip_rated, " kg.cm rated"));
echo(str("roll joint: ", round(roll_need*10)/10, " kg.cm now; ", round(roll_stand*10)/10, " kg.cm just standing, from the wheel offset"));
echo(str("wheel rim force at stall: ", round(rim_force*10)/10, " kg against ", mass_kg, " kg"));
echo(str("bed check (180 mm): body ", body_length, ", wheel ", wheel_diameter, ", leg ", leg_print, leg_print > 180 ? " -- lay the leg diagonally" : ""));
echo("fasteners: 4x M3 heat-set insert, 4x M3x10 (lid); servo M2 screws for hips, brackets, legs and hubs; tyre snaps on");
// ---------- primitives ----------
module rbox(l, w, h, r) {
hull() for (sx = [-1, 1], sy = [-1, 1])
translate([sx * (l / 2 - r), sy * (w / 2 - r), 0]) cylinder(h = h, r = r);
}
// Servo with shaft along +z, shaft centre at the origin, case below z=0.
module servo(sz) {
color("Goldenrod") {
translate([-shaft_off, -sz[1] / 2, -sz[2]]) cube([sz[0], sz[1], sz[2]]);
cylinder(h = 4, d = 6);
}
}
module horn_holes(h) {
for (a = [0 : 90 : 270]) rotate([0, 0, a])
translate([horn_circle / 2, 0, 0]) cylinder(h = h, d = horn_hole, center = true);
}
// ---------- body tray ----------
module battery_rib() {
difference() {
translate([-23, -1.5, 0]) cube([46, 3, wall + 14]);
for (i = [-1, 1]) translate([i * 10.5, 0, wall + 11])
rotate([90, 0, 0]) cylinder(h = 5, d = 19, center = true);
}
}
module body_tray() {
difference() {
union() {
difference() {
rbox(body_length, body_width, split_z, corner_r);
translate([0, 0, wall]) rbox(body_length - 2 * wall, body_width - 2 * wall, split_z, corner_r - wall);
}
for (sx = [-1, 1], sy = [-1, 1])
translate([sx * (body_length / 2 - corner_in), sy * (body_width / 2 - corner_in), 0]) cylinder(h = split_z, d = 9);
for (sx = [-1, 1], sy = [-1, 1])
translate([pi_x + sx * 29, sy * 11.5, 0]) cylinder(h = wall + 5, d = 5.5);
for (sx = [-1, 1])
translate([drv_x + sx * 20, 0, 0]) cylinder(h = wall + 5, d = 5.5);
for (sy = [-1, 1]) translate([bat_x, sy * 30, 0]) battery_rib();
}
for (sx = [-1, 1], sy = [-1, 1])
translate([sx * (body_length / 2 - corner_in), sy * (body_width / 2 - corner_in), split_z - 8]) cylinder(h = 9, d = insert_d);
for (sx = [-1, 1], sy = [-1, 1])
translate([pi_x + sx * 29, sy * 11.5, wall]) cylinder(h = 6, d = m2_hole);
for (sx = [-1, 1])
translate([drv_x + sx * 20, 0, wall]) cylinder(h = 6, d = m2_hole);
// roll servo mounting screws through the side walls
for (side = [-1, 1], hx = [-(rH - side_hole_in), -side_hole_in], hz = [shaft_off - side_hole_in, -(rL - shaft_off - side_hole_in)])
translate([hx, side * body_width / 2, hip_z + hz]) rotate([90, 0, 0]) cylinder(h = wall * 3, d = m2_hole, center = true);
// vents, front and rear thirds of each side
for (side = [-1, 1], i = [0 : 3]) {
translate([body_length / 2 - 16 - i * 9, side * body_width / 2, split_z / 2 + 4]) rotate([0, 20, 0]) cube([3, wall * 3, 18], center = true);
translate([-body_length / 2 + 16 + i * 9, side * body_width / 2, split_z / 2 + 4]) rotate([0, -20, 0]) cube([3, wall * 3, 18], center = true);
}
// rear switch or charge lead
translate([-body_length / 2, 0, split_z - 12]) cube([wall * 3, 14, 9], center = true);
}
}
// ---------- body lid ----------
module body_lid() {
difference() {
union() {
hull() {
rbox(body_length, body_width, 1, corner_r);
translate([0, 0, lid_h - 1]) rbox(body_length - 10, body_width - 10, 1, corner_r - 4);
}
translate([0, 0, -4]) rbox(body_length - 2 * wall - 0.6, body_width - 2 * wall - 0.6, 4.01, corner_r - wall);
for (sx = [-1, 1], sy = [-1, 1])
translate([sx * (body_length / 2 - corner_in), sy * (body_width / 2 - corner_in), -4]) cylinder(h = lid_h + 4, d = 9);
if (sensor == "lidar" || sensor == "both")
translate([lidar_x, 0, lid_h - 0.01]) cylinder(h = 3, d = 44);
for (sx = [-1, 1]) translate([sx * 9, 0, wall]) cylinder(h = lid_h - 2 * wall - 4, d = 5);
}
translate([0, 0, -5]) rbox(body_length - 4 * wall - 0.6, body_width - 4 * wall - 0.6, lid_h - wall + 5, corner_r - 2 * wall);
for (sx = [-1, 1], sy = [-1, 1]) {
translate([sx * (body_length / 2 - corner_in), sy * (body_width / 2 - corner_in), -5]) cylinder(h = lid_h + 10, d = m3_clear);
translate([sx * (body_length / 2 - corner_in), sy * (body_width / 2 - corner_in), lid_h - 3.5]) cylinder(h = 6, d = m3_head);
}
if (sensor == "lidar" || sensor == "both")
translate([lidar_x, 0, lid_h - 10]) {
cylinder(h = 20, d = 12);
for (a = [45 : 90 : 315]) rotate([0, 0, a]) translate([15, 0, 0]) cylinder(h = 20, d = 2.7);
}
if (sensor == "camera" || sensor == "both")
translate([body_length / 2 - 5, 0, lid_h / 2]) cube([16, 26, 12], center = true);
for (sx = [-1, 1]) translate([sx * 9, 0, wall]) cylinder(h = 5, d = m2_hole);
}
}
// ---------- hip bracket (right leg frame) ----------
module hip_bracket() {
difference() {
union() {
translate([horn_thick - pt, -rW / 2 - 1, shelf_top - pt])
cube([pt, rW + 2, shaft_off + 4 - (shelf_top - pt)]);
translate([-14, -rW / 2 + 0.5, shelf_top - pt])
cube([rL - shaft_off + 16, rH + 0.5, pt]);
// gusset between plate and shelf on the outboard edge
hull() {
translate([horn_thick - pt, rW / 2 - 2, shelf_top]) cube([pt, 3, 12]);
translate([horn_thick - pt, rW / 2 - 2, shelf_top - pt]) cube([12, 3, pt]);
}
}
rotate([0, 90, 0]) { horn_holes(20); cylinder(h = 20, d = 8, center = true); }
for (hx = [-shaft_off + side_hole_in, rL - shaft_off - side_hole_in], hy = [pitch_face_y - rH + side_hole_in, pitch_face_y - side_hole_in])
translate([hx, hy, shelf_top - pt - 1]) cylinder(h = pt + 2, d = m2_hole);
}
}
// ---------- leg (pitch axis at origin, plate normal y, leg down -z) ----------
module leg() {
n = max(1, floor((leg_length - 70) / 30));
slot = (leg_length - 70) / n - 6;
difference() {
hull() {
rotate([90, 0, 0]) cylinder(h = lt, d = 34, center = true);
translate([0, 0, -leg_length]) rotate([90, 0, 0]) cylinder(h = lt, d = 30, center = true);
}
rotate([90, 0, 0]) { horn_holes(20); cylinder(h = 20, d = 8, center = true); }
for (i = [0 : n - 1])
translate([0, 0, -35 - slot / 2 - i * (slot + 6)])
rotate([90, 0, 0]) hull() {
translate([0, slot / 2 - 5, 0]) cylinder(h = lt * 3, d = 10, center = true);
translate([0, -slot / 2 + 5, 0]) cylinder(h = lt * 3, d = 10, center = true);
}
translate([0, 0, -leg_length]) rotate([90, 0, 0]) {
cylinder(h = 20, d = 8, center = true);
for (sx = [-1, 1], sz = [-1, 1]) translate([sx * face_hx, sz * face_hz, 0]) cylinder(h = 20, d = m2_hole, center = true);
}
}
}
// ---------- wheel hub and tyre (axis z, hub face at z=0) ----------
rim_d = wheel_diameter - 2 * tire_t;
module wheel_hub() {
web_t = 6;
difference() {
union() {
difference() {
cylinder(h = wheel_width, d = rim_d);
translate([0, 0, -1]) cylinder(h = wheel_width + 2, d = rim_d - 6);
}
cylinder(h = web_t, d = rim_d - 5);
}
for (a = [0 : 72 : 359]) rotate([0, 0, a + 36]) translate([0, 0, -1]) hull() {
translate([0, 22, 0]) cylinder(h = web_t + 2, d = 10);
translate([0, rim_d / 2 - 14, 0]) cylinder(h = web_t + 2, d = 20);
}
translate([0, 0, wheel_width / 2]) rotate_extrude() translate([rim_d / 2, 0]) circle(d = 3);
translate([0, 0, -1]) cylinder(h = web_t + 2, d = 8);
translate([0, 0, web_t / 2]) horn_holes(web_t + 2);
}
}
module tire() {
color("DimGray") difference() {
rotate_extrude() hull() {
translate([rim_d / 2 + 0.2, 0]) square([0.01, wheel_width]);
translate([wheel_diameter / 2 - 4, 4]) circle(r = 4);
translate([wheel_diameter / 2 - 4, wheel_width - 4]) circle(r = 4);
}
for (i = [0 : tread_n - 1]) rotate([0, 0, i * 360 / tread_n])
translate([wheel_diameter / 2, 0, wheel_width / 2]) rotate([15, 0, 0]) cube([4, 2.2, wheel_width * 2], center = true);
}
color("DimGray") translate([0, 0, wheel_width / 2]) rotate_extrude() translate([rim_d / 2 + 0.2, 0]) circle(d = 2.6);
}
// ---------- assembly ----------
module right_leg_assembly() {
rotate([0, 90, 0]) servo(hip_size);
rotate([leg_roll, 0, 0]) {
color("Silver") hip_bracket();
translate([0, pitch_face_y, pitch_z]) rotate([-90, 0, 0]) servo(hip_size);
translate([0, 0, pitch_z]) rotate([0, -leg_pitch, 0]) {
color("Silver") translate([0, leg_y_in + lt / 2, 0]) leg();
translate([0, leg_y_in, -leg_length]) rotate([-90, 0, 0]) servo(whl_size);
translate([0, leg_y_in + lt + horn_thick, -leg_length]) rotate([-90, 0, 0]) {
color("Gainsboro") wheel_hub();
tire();
}
}
}
}
module assembly() {
color("SteelBlue") body_tray();
color("SteelBlue") translate([0, 0, split_z]) body_lid();
if (sensor == "lidar" || sensor == "both")
color("DimGray") translate([lidar_x, 0, body_height + 3]) cylinder(h = 33, d = 38);
translate([0, roll_y, hip_z]) right_leg_assembly();
mirror([0, 1, 0]) translate([0, roll_y, hip_z]) right_leg_assembly();
color("Gainsboro") translate([0, 0, ground_z - 3]) cube([500, track_w + 160, 6], center = true);
}
if (part == "assembly") assembly();
else if (part == "body_tray") body_tray();
else if (part == "body_lid") translate([0, 0, lid_h]) mirror([0, 0, 1]) body_lid();
else if (part == "hip_bracket") translate([0, 0, horn_thick]) rotate([0, 90, 0]) hip_bracket();
else if (part == "leg") translate([0, 0, lt / 2]) rotate([90, 0, 0]) leg();
else if (part == "wheel_hub") wheel_hub();
else if (part == "tire") tire();
Comments
Context, not a score. Comments never move a model up or down the results.No comments yet
Printed it? A note about your material and the clearance you landed on saves the next person a failed print.
