This repository contains the 3D model source files for a four-wheeled robot chassis and steering system, written entirely in OpenSCAD. The design supports 3D printing and does not rely on external libraries.
Table of Contents
- About
- Overview
- Ackermann Geometry
- Assembly
- Structure
- External Details
- Bearings
- Servos
- Raspberry Pi
- Power Supply
- Motors
- Camera Module
- Ultrasonic
- Switch buttons and fuses
- IR LED
- Bolts, Nuts and Standoffs
- Switch buttons and fuses
- Full BOM (default preset)
- Full BOM (with UPS module S3)
The robot model is designed around the following core elements:
- Ackermann Steering: This design implements Ackermann steering and computes the steering radii from the robot parameters (not just by making the inner wheel turn more). The model calculates the trapezoid geometry automatically so you usually don’t need to tweak Ackermann-specific variables by hand.
- Rear-Wheel Drive: Two individual motors drive the rear wheels. Both standard yellow DC motors and N20-type motors are supported.
- Modular Head Mount: The head mount is designed to accommodate two Raspberry Pi Camera Module 3 or more sensors (e.g., day/night configuration).
- Extendable Power Tiers: Default power case for high-discharge LiPo packs (e.g., Turnigy Rapid 4S2P) plus battery holders plus slots for servo/motor driver HATs; Waveshare UPS S3 remains optional.
- Raspberry Pi: The chassis includes placements and bolt holes for the Raspberry Pi 5 and multiple 18650 battery holders.
Since textmetrics is used throughout the project, a nightly build of OpenSCAD is required. Also, be sure to enable textmetrics in the OpenSCAD editor via Preferences -> Features -> textmetrics. When using the CLI, pass --enable=textmetrics (e.g., openscad --enable=textmetrics --backend=Manifold ...).
make help— list available targets.make tests— run alltests/test_*.scadsuites; tolerates the empty top-level warning used by logic-only tests.make assembly— exportscad/assembly.scadtobuild/export/stl/assembly.stlandbuild/export/3mf/assembly.3mfwith--backend=Manifold --enable=textmetrics --hardwarningsand 3MF metadata.make printable— exportscad/printable.scadand every file inscad/printable_parts/to flattenedbuild/export/stl/andbuild/export/3mf/(tires prefer the TPU material hint; falls back if unsupported).make all— run tests, then build assembly and all printable exports.make clean— remove build outputs and test temp files.
Two main presets are supported: the default LiPo power case stack and the UPS S3 option. Both assume two battery holders. Jump to the BOMs for details: Full BOM (default preset) or Full BOM (with UPS module S3).
All commands require --enable=textmetrics --backend=Manifold.
# Full assembly preview (PNG)
openscad -o /tmp/scad-preview.png --preview --imgsize=754,934 --backend=Manifold --enable=textmetrics --camera=0,0,0,0,0,0,1100 scad/assembly.scad
# Utility tests
openscad --backend=Manifold --enable=textmetrics -o /tmp/test_functions.stl tests/test_functions.scad
openscad --backend=Manifold --enable=textmetrics -o /tmp/test_plist.stl tests/test_plist.scadAckermann steering is implemented with a rack-and-pinion mechanism that drives a single L-link (rack_link) mounted to one knuckle. That knuckle rotates when the rack moves, the opposite knuckle follows through a tie rod. The knuckles and tie rod form the Ackermann trapezoid, so the inside wheel turns more than the outside wheel.
Note
Install the rack_link on only one side and on only one of the knuckles - it doesn't matter which. Movement of the rack will cause that "leading" knuckle to rotate. The leading knuckle is then connected to the second, "driven" knuckle via a tie rod.
Most parameters live in scad/parameters.scad, but the actual Ackermann geometry (angles and the required tie-rod top width) is calculated automatically from core robot dimensions such as chassis length, steering panel placement and knuckle geometry. Because of that, you will rarely need to edit Ackermann-specific variables manually - especially steering_angle_deg, which is derived from the layout.
The interactive guide lives in scad/assembly_guide.scad. Open it in OpenSCAD and step through the boolean checkboxes in the built-in Customizer to reveal each assembly step (power case, steering, electronics, and wheels). For a static full build view, use scad/assembly.scad.
The project is organized into several reusable modules under the scad/ directory:
parameters.scad: Central configuration file containing physical dimensions (units are in millimeters).power_lid_parameters.scad: Configuration for the power case lid.printable.scad: Contains all printable parts in one place. You can print all parts except the tires using either PETG (recommended) or PLA. For the tires, use TPU (e.g., TPU 95A).
printable_parts/: Individual printable part sources (one part per file) used by the Makefile for per-part exports.assembly.scad: Fully assembled view of the robot.
assembly_guide.scad: Interactive assembly. See above.steering_system/: Rack-and-pinion steering system based on Ackermann geometry.power/: Power case and related components.components/chassis/: Chassis and other components.head/: Mounting system for dual Raspberry Pi cameras.motor_brackets/: Brackets for both standard (yellow) and N20-style motors.wheels/: Components for rear and front wheels, including hubs and tires.placeholders/: Placeholder geometry for components such as the Raspberry Pi, servos, DC motors, battery holders, HATs, sensors, step-down voltage converters, INA260, etc. There’s a lot of stuff in there.lib/: Reusable modules. One important module islib/plist, which provides helpers for working with property lists-a dict-like data structure used throughout the project because OpenSCAD doesn’t support dictionaries.
All these details are just recommendations, you can use any other details, just don't forget to specify corresponding dimensions in parameters.scad and power_lid_parameters.scad. Reference renders and photos live in demo/ (e.g., demo/top-view.png, demo/ups_module_s3.png, demo/head_neck.png, demo/ultrasonic.png).
- Four 685 (5x11x5) bearings: two are inserted into the steering knuckles and mounted on kingpin posts. To use different sizes, modify the variables
knuckle_bearing_outer_dia,knuckle_bearing_inner_dia, and other relevant variables. The other two are inserted into the tie rod; the corresponding variables aretie_rod_bearing_outer_dia,tie_rod_bearing_inner_dia, and others. - Two 693 (3x8x4) bearings are inserted into the rack link and into the knuckle arm that connects to the rack link. Both flanged and standard versions are supported.
- Two 608 (8x22x7) bearings are inserted into the front wheels.
This project supports three EMAX ES08MA II servos by default. If using different models, update the following variables in parameters.scad:
steering_servo_slot_widthandsteering_servo_slot_heightfor the steering servohead_neck_pan_servo_slot_widthandhead_neck_pan_servo_slot_heightfor the pan servohead_neck_tilt_servo_slot_widthandhead_neck_tilt_servo_slot_heightfor the tilt servo
Raspberry Pi 5 is supported by default. To use a different model, adjust the relevant variables prefixed with rpi_ (e.g., rpi_bolt_spacing).
The default power source is a modular power case sized for a Turnigy Rapid-style hardcase LiPo (see the lipo_pack_* and power_case_* parameters). The lid supports an XT90E connector, blade fuses, a voltmeter, and a step-down regulator. Rails/clamps keep the pack fixed.
The Waveshare UPS module S3 is still supported, but it is no longer the default because it is not reliable enough.
If you want to use it, enable it in parameters.scad:
battery_ups_holes_enabled = true;And in assembly.scad:
show_ups_hat = true;You will likely also need to adjust the layout parameters for the Raspberry Pi (rpi_chassis_y_position and rpi_chassis_x_position) and perhaps disable the power case module (see the power_* variables).
- Toggle UPS on/off: set
battery_ups_holes_enabledinparameters.scadandshow_ups_hatinassembly.scad. - Enable/disable the power case stack via the
power_*variables when switching to UPS. - Reposition the Raspberry Pi for UPS clearance using
rpi_chassis_y_positionandrpi_chassis_x_position. - Keep two battery holders enabled by default (adjust
chassis_body_battery_holders_specsif you change count/placement).
Battery holders with different settings are supported; the presets assume two holders by default. Variables prefixed with battery_holder* control the default values, so you can combine different holders and settings by editing chassis_body_battery_holders_specs, a property list that takes precedence over the global variables.
By mount type
battery_holder_mount_type holds the global mount type: either under_cell (holes under each cell intercell) or intercell (holes between the cells). The default is intercell.
By terminal type
battery_holder_terminal_type holds the global terminal type: solder_tab (rectangular external tabs) or coil_spring (circular contact with a helical spring, positive polarity). solder_tab (default) will also create additional slots for the contacts in the chassis.
You can create as many battery holders and slots for them as you want. Here is a quick example:
chassis_body_battery_holders_specs =
["type", "grid",
"size", [chassis_body_w, chassis_body_len],
"rows",
maybe_add_battery_holders_rows_h(
[["cells",
[["w", 0.5,
"placeholder",
["placeholder_type", "battery_holder",
"mount_type", battery_holder_mount_type,
"count", 3,
"show_battery", true,
"terminal_type", battery_holder_terminal_type,
"side_wall_cutout_type", battery_holder_side_wall_type]],
["w", 0.5,
"placeholder",
["placeholder_type", "battery_holder",
"terminal_type", "coil_spring",
"side_wall_cutout_type", "enclosed",
"battery_len", 70,
"battery_dia", 21,
"show_battery", true,
"mount_type", "under_cell",
"battery_color", "pink",
"color", "black"]]]],
["cells",
[["w", 0.5,
"spin", 90,
"placeholder",
["placeholder_type", "battery_holder",
"show_battery", true,
"count", 1,
"mount_type", battery_holder_mount_type,
"terminal_type", battery_holder_terminal_type,
"side_wall_cutout_type", battery_holder_side_wall_type]]]]])];Result:
Two rear motors are supported-either standard or N20-type:
- The "n20" type refers to motors like the GA12-N20 with a 3 mm shaft.
- The "standard" type refers to widely available yellow gear motors with a 5 mm shaft.
Rear wheel shaft size depends on the motor type. Use the variable motor_type in parameters.scad to set the proper shaft diameter.
The design supports one, two or more camera modules. The default dimensions are compatible with the Raspberry Pi Camera Module 3.
The model supports the HC-SR04 ultrasonic sensor.
Switch buttons with a circular mounting type are supported, as well as two types of fuse holders: ATM and ATC mini.
The design also supports case for IR Waveshare Infrared LED Light Board Module.
The original LED board is incompatible with the Raspberry Camera Module 3, and the quality of Waveshare’s original camera is unsatisfactory.
Nevertheless, this LED board can be used with Camera Module 3 and other Raspberry Pi cameras. To do so, solder two wires (GND and V+) to the bolt holes on the LED board (the bolt holes serve both for mechanical attachment and for power). Then connect the positive wire to 3.3V and the ground wire to GND.
Since the default chassis thickness is 4 mm (changeable via the variable chassis_thickness), use bolts at least 8 mm long for fastenings that go through the chassis, depending on the nuts. I suggest using lock nuts for these bolts, but plain nuts are also acceptable.
The tables below are grouped by component. Pick only the sections relevant to your build.
Each table includes a "Variable" column that refers to a configurable variable in parameters.scad. You can change it to the bolt diameter you want to use.
| Size | Length (mm) | Amount | Nuts | Variable |
|---|---|---|---|---|
| M2.5 | 8 | 4 | 2 | steering_panel_hinge_bolt_dia |
| M2 | 8 | 2 | 2 | steering_servo_bolt_dia |
| M3 | 20 | 2 | 2 | steering_servo_mount_connector_bolt_dia |
| M2 or M2.5 for tight fit | 10 | 4 | 4 | steering_kingpin_post_bolt_dia |
Your servo pack should already include bolts, a servo arm, and self-tapping bolts to secure the servo arm, but I'll list them here for reference as well.
| Size | Length (mm) | Amount | Nuts | Variable |
|---|---|---|---|---|
| M2 | 4 | 1 | 0 | steering_servo_bolt_dia |
Tip
The variable steering_servo_bolt_dia is used only in assembly views, but there are useful variables that define the diameter of the hole at the center of the pinion where you insert the servo's gearbox - steering_pinion_center_hole_dia. Its default size is 6.5 mm.
Self-tapping bolts should also be included in your pack. The diameter may differ slightly, but the bolts should still fit due to the nature of self-tapping bolts.
| Size | Length (mm) | Amount | Nuts | Variable |
|---|---|---|---|---|
| M1.5 | 4 | 2 or more | 0 | steering_pinion_screw_dia |
| Size | Length (mm) | Amount | Nuts | Variable |
|---|---|---|---|---|
| M2.5 | 10 | 4 (2 per knuckle) | 0 | knuckle_shaft_bolt_dia |
| M2 or M2.5 for tight fit | 10 | 4 (2 per knuckle) | 0 | tie_rod_shaft_bolt_dia |
The exact bolt length depends on the standoffs you use. Since the default chassis thickness is 4 mm, you should use bolts that are at least 6 mm long.
| Size | Length (mm) | Amount | Nuts/Standoffs | Variable |
|---|---|---|---|---|
| M2 | 6 or higher | 4 | 4 (both nuts and standoffs) | rpi_bolt_hole_dia |
Power case stack consists of four components:
- The socket case at the bottom, which holds the XT90E-M male connector and the fuse holder. It has four mounting holes for M3 standoffs that will secure it to the chassis.
- The socket’s sliding lid, also with four holes of the same spacing and size.
- The power case for a LiPo pack (e.g., Turnigy Rapid). It also has four holes of the same spacing and size as the socket components. It is mounted above the socket’s sliding lid and secured with four long M3 bolts that thread into the socket’s standoffs.
- The power case lid, with slots for a DC voltage regulator, voltmeters, a fuse holder, etc. Four M2.5 bolts clamp the lid rails, keep them short to avoid piercing the thin side walls.
| Size | Length (mm) | Amount | Standoffs | Variable |
|---|---|---|---|---|
| M3 | 31 or higher | 4 | 4 | power_case_bottom_bolt_dia |
| Size | Length (mm) | Amount | Nuts | Variable |
|---|---|---|---|---|
| M3 | 12 or higher | 2 | 2 | xt90e_mount_dia or power_socket_case_jack_plist 1 |
Bolts:
| Size | Length (mm) | Amount | Nuts | Variable |
|---|---|---|---|---|
| M2.5 | 6 | 4 | 4 | power_case_rail_bolt_dia |
Step-down voltage regulator slot on the lid:
By default, the slot fits the Pololu D24VXF5 step-down voltage regulator.
| Size | Length (mm) | Amount | Standoffs | Variable |
|---|---|---|---|---|
| M2 | 4 or higher | 4 | 4 | step_down_voltage_bolt_hole_dia |
Voltmeters:
By default, there are five slots for voltmeters, with two holes for each. To change the number of bolts to four, edit power_lid_left_slots in scad/power_lid_parameters.scad. Find the entry with the "placeholder" property set to "voltmeter". Change slot_size to the desired spacing.
The default slot_size is [0, 27.70] (the X and Y spacing for the four corner mounting bolt positions). If you change it, for example, to [10, 27.70], four holes will be created.
| Size | Length (mm) | Amount | Standoffs | Variable |
|---|---|---|---|---|
| M3 | 4 or higher | 2 | 2 (per slot) | voltmeter_bolt_dia |
| Size | Length (mm) | Amount | Nuts | Variable |
|---|---|---|---|---|
| M3 | 8 or higher | 2 (per one) | 2 | battery_holder_bolt_dia |
You should use either 4 or 8 M3 standoffs. If you use 8 standoffs, the nuts listed below are not needed.
| Size | Length (mm) | Amount | Nuts | Variable |
|---|---|---|---|---|
| M3 | 8 or higher | 4 | 4 | battery_ups_bolt_dia |
| Size | Length (mm) | Amount | Nuts | Variable |
|---|---|---|---|---|
| M2.5 | 8 or higher | 4 | 4 | n20_motor_bolt_dia |
| Size | Length (mm) | Amount | Nuts | Variable |
|---|---|---|---|---|
| M3 | 28 | 4 | 4 | standard_motor_bracket_motor_bolt_hole |
| M2 | 8 or higher | 4 | 4 | standard_motor_bracket_chassis_bolt_hole |
| Size | Length (mm) | Amount | Nuts | Variable |
|---|---|---|---|---|
| M2 | 5 or higher | 8 (4 for each camera) | 8 | head_camera_bolt_dia |
As with the steering servo, your servo pack should already include bolts, a servo arm, and self-tapping bolts to secure the servo arm. The diameter of the hole on the head where the tilt-servo gearbox mounts is defined by head_servo_mount_dia.
The corresponding hole for the pan servo on the chassis is controlled by chassis_pan_servo_slot_dia.
| Size | Length (mm) | Amount | Nuts | Variable |
|---|---|---|---|---|
| M2 | 6 or higher | 2 | 2 | head_neck_pan_servo_bolt_dia |
| M2 | 6 or higher | 2 | 2 | head_neck_tilt_servo_bolt_dia |
| Size | Length (mm) | Amount | Nuts | Variable |
|---|---|---|---|---|
| M2 | 7 or higher | 2 | 2 | ir_case_bolt_dia |
| M2 | 10 or higher | 2 | 2 | ir_case_rail_bolt_dia |
Variable ir_case_bolt_dia defines the diameter of the bolt holes that attach the case to the head; ir_case_rail_bolt_dia defines the diameter of the rail holes that secure the IR LED to the case itself.
| Size | Length (mm) | Amount | Nuts | Variable |
|---|---|---|---|---|
| M2.5 | 6 or higher | 2 | 2 | front_panel_connector_bolt_dia |
| M2.5 | 10 or higher | 2 | 2 | front_panel_bolt_dia |
front_panel_connector_bolt_dia defines the diameter of the bolt hole on the bracket that connects to the chassis, and front_panel_bolt_dia defines the diameter of the bolt hole on the front panel that secures the ultrasonic sensor, which is placed between the front panel and the bracket.
Each wheel hub requires 6 M2.5 (or M3 for a very tight fit) bolts and nuts.
| Size | Length (mm) | Amount | Nuts | Variable |
|---|---|---|---|---|
| M2.5 or M3 for tight fit | 12 or higher | 12 (6 per wheel hub) | 12 | wheel_hub_bolt_dia |
Bolts
| Size | Length (mm) | Amount | Nuts | Variable |
|---|---|---|---|---|
| M3 | 6 or higher | 4 | 4 | panel_stack_bolt_dia |
Standoffs
Since the two panels are connected with standoffs, the required standoff height depends on the height of the fuse holders and switch buttons. Usually, you should use 6-8 standoffs (12-15 mm) for the button panel and four 12 mm standoffs for the fuse panel.
Assumes two battery holders are installed alongside the default LiPo power case stack. Currently using Waveshare 16-channel Servo Driver HAT and Waveshare RPi Motor Driver Board, but planning/recommending Pololu Dual MC33926 Motor Driver for Raspberry Pi and a Pololu servo driver HAT (model TBD).
Core components
- Printed parts from
scad/printable.scad(PETG/PLA) and TPU tires - 1x Raspberry Pi 5
- 3x EMAX ES08MA II servos (steering + pan + tilt)
- 2x N20 rear motors (default
motor_type) - 2x Raspberry Pi Camera Module 3
- 1x HC-SR04 ultrasonic sensor
- 1x IR Waveshare Infrared LED Light Board Module
- 1x Servo driver HAT, 1x motor driver HAT (see note above)
- 4x 685 bearings, 2x 693 bearings, 2x 608 bearings
- Turnigy Rapid-style 4S2P LiPo + XT90E connector, Pololu D24VXF5 regulator, 5x voltmeters, 4x fuse holders (between LiPo and toggle switch, toggle switch and RPi, one each for motor/servo HATs), 3x toggle switches
- 2x 18650 battery holders (see
battery_holder*parameters)
Hardware summary (see bolt lengths in the section above)
Fuse routing: LiPo -> fuse -> toggle -> fuse -> RPi; dedicated fuses feed the motor and servo HATs.
| Subassembly | Bolts | Nuts | Standoffs/Notes |
|---|---|---|---|
| Steering panel | M2.5×8 (4), M2×8 (2), M3×20 (2), M2/M2.5×10 (4) | 10 | — |
| Steering pinion | M2×4 (1), M1.5 self-tapping (2) | 0 | Uses servo arm hardware |
| Steering knuckles | M2.5×10 (4), M2/M2.5×10 (4) | 0 | — |
| Raspberry Pi | M2×6+ (4) | 4 | 4× standoffs |
| Power case stack (LiPo, socket, lid, regulator, voltmeters) | M3×31+ (4), M3×12+ (2), M2.5×6 (4), M2×4+ (4), M3×4+ for voltmeters (10) | 6 | 4× long M3 standoffs, 4× for regulator, 10× for voltmeters |
| Battery holders on chassis (2×) | M3×8+ (4) | 4 | — |
| Switch buttons & fuse panels | M3×6+ (4) | 4 | 6–8 standoffs (12–15 mm) for button panel, 4×12 mm for fuse panel |
| Motor brackets (N20 pair) | M2.5×8+ (4) | 4 | — |
| Head (2 cameras) | M2×5+ (8) | 8 | — |
| Head neck | M2×6+ (4) | 4 | — |
| IR case | M2×7+ (2), M2×10+ (2) | 4 | — |
| Ultrasonic bracket | M2.5×6+ (2), M2.5×10+ (2) | 4 | — |
| Wheel hubs (pair) | M2.5×12+ (12) | 12 | Use M3 for tighter fit |
Assumes two battery holders are installed; this variant omits the LiPo power case stack in favor of the UPS module S3. Currently using Waveshare 16-channel Servo Driver HAT and Waveshare RPi Motor Driver Board, but I am planning and recommending Pololu Dual MC33926 Motor Driver for Raspberry Pi and a Pololu servo driver HAT (model TBD).
Core components
- Printed parts from
scad/printable.scad(PETG/PLA) and TPU tires - 1x Raspberry Pi 5
- 3x EMAX ES08MA II servos (steering + pan + tilt)
- 2x N20 rear motors (default
motor_type) - 2x Raspberry Pi Camera Module 3
- 1x HC-SR04 ultrasonic sensor
- 1x IR Waveshare Infrared LED Light Board Module
- 1x Servo driver HAT, 1x motor driver HAT (see note above)
- 4x 685 bearings, 2x 693 bearings, 2x 608 bearings
- Waveshare UPS module S3 (+ 4 or 8 M3 standoffs), 3x toggle switches, 2x fuse holders (one each for motor/servo HATs), 5x voltmeters
- 2x 18650 battery holders (see
battery_holder*parameters)
Hardware summary (see bolt lengths in the section above)
Fuse routing: UPS -> fuse -> RPi; dedicated fuses feed the motor and servo HATs.
| Subassembly | Bolts | Nuts | Standoffs/Notes |
|---|---|---|---|
| Steering panel | M2.5×8 (4), M2×8 (2), M3×20 (2), M2/M2.5×10 (4) | 10 | — |
| Steering pinion | M2×4 (1), M1.5 self-tapping (2) | 0 | Uses servo arm hardware |
| Steering knuckles | M2.5×10 (4), M2/M2.5×10 (4) | 0 | — |
| Raspberry Pi | M2×6+ (4) | 4 | 4× standoffs |
| UPS module S3 mount | M3×8+ (4) | 4 | Use 4 or 8 M3 standoffs; nuts not needed with 8 standoffs |
| Battery holders on chassis (2×) | M3×8+ (4) | 4 | — |
| Switch buttons & fuse panels | M3×6+ (4) | 4 | 6–8 standoffs (12–15 mm) for button panel, 4×12 mm for fuse panel |
| Motor brackets (N20 pair) | M2.5×8+ (4) | 4 | — |
| Head (2 cameras) | M2×5+ (8) | 8 | — |
| Head neck | M2×6+ (4) | 4 | — |
| IR case | M2×7+ (2), M2×10+ (2) | 4 | — |
| Ultrasonic bracket | M2.5×6+ (2), M2.5×10+ (2) | 4 | — |
| Wheel hubs (pair) | M2.5×12+ (12) | 12 | Use M3 for tighter fit |
Footnotes
-
xt90e_mount_diais a default value used inpower_socket_case_jack_plist, which is a property list (a dict-like data structure) used throughout the project because OpenSCAD doesn’t support dictionaries. Unlike JSON, it uses a comma (,) instead of a colon (:). To change the diameter of the mounting holes for the XT90E-M connector only for the socket case, edit themount_diaproperty inpower_socket_case_jack_plist. ↩












