
OmArm One Series Part 1: Build, statics and payload (this post) · Part 2: Digital twin (URDF, the robot-description format ROS uses, in RViz and Gazebo) (coming soon) · Part 3: MoveIt 2 on real hardware (coming soon) · Part 4: Computer vision pick and place (coming soon) · Part 5: Grasping without markers (coming soon)
New here? This arm is the successor of اوم ارم زيرو, the $50 build this series grew out of. You don’t need Zero to follow along, but the design decisions below will make more sense if you know where they came from.
This is a 3D printed robot arm with six joints and a gripper, and the thing that separates it from most printed arms is what carries the load. Every joint that takes a bending moment runs on a steel ball bearing rather than on the servo’s own gear train. The base sits on a 51110 thrust bearing and a 6806ZZ radial bearing; the pitch joints use a fixed-loose arrangement around a 695ZZ. The result reaches 485 mm, carries 200 g nominal and 250 g maximum at a 300 mm working radius, and weighs 1283 g on a kitchen scale.
Everything below is derived rather than asserted. The torque numbers come out of a mass model that scans the whole joint space for each joint’s worst pose, the payload table is that same model at a fixed utilisation, and the analysis script that produced them ships with the repository. Where I got something wrong, it says so and shows the corrected number.
At a glance
What you’ll build: A 6-joint robotic arm with a parallel-jaw gripper. 485mm reach, 200g nominal and 250g maximum payload at a 300mm working radius, ball bearings carrying the load at every joint that needs them. Motion that starts and stops without a lurch, a safety layer that refuses to move blind after a reset, and an electronics stack that Part 2 hands straight to ROS 2. You drive it from your phone, over a WiFi network the arm brings up itself, with nothing installed on either side.
Where it leads: Part 1 of five; section 3 lays out where the other four take it.
Time required: 15 h 6 min of printing across two plates on a P1S, and that is unattended time. Assembly and testing add about three hours on top of that.
Total cost: ca. 210 to 360 € at August 2026 street prices if you own none of it and buy every assortment pack whole. The parts that actually end up inside the arm are worth ca. 160 to 270 €, and a maker who already has a printer, filament, a 6 V supply and a drawer of M3 hardware gets there for ca. 140 to 250 €. In every one of those views the six DS3240 servos are the biggest single block at 40 to 43 %. A different budget class than Zero’s $50, and section 6 breaks it down line by line.
Difficulty: Intermediate. If you built OmArm Zero, this is the same kind of work with heavier parts, real bearings, and higher stakes at the base joint.
Safety: The arm can pinch hard enough to hurt, and the supply pushes real current. Section 7 starts with five habits that cover it. Read them before the first power-up rather than after.
What you should already be comfortable with: flashing an ESP32 from the Arduino IDE and slicing an STL. You will also be reading a caliper more than once. You do not need to know robotics maths. Where this post uses a term from that world, it defines it the first time. Stall torque is the twist a servo can hold before it stops turning. A horn is the splined plastic arm that clamps onto the servo’s output shaft, and backlash is the free play you feel when you wiggle a gear train that is supposed to be locked. Sections 4 و 5 are the theory, and you can skip them on a first read and come back when a number in the build surprises you.
Tools you’ll need: This comes off the fastener list in section 6, so it is what the drawing calls for rather than what a finished build taught me:
- 2.5 mm hex key. The fourteen M3 socket-head screws all take the same one
- 8 mm spanner or socket for the three M5 bolts and their nuts
- Phillips PH0 and PH1, between them covering the 63 self-tapping screws and the M1.6 in the gripper
- Pozidriv PZ1, for the two M3 screws that are pozidriv rather than socket-head
- A caliper, for the step in section 7 where you measure a bearing seat before anything gets pressed into it
- Soldering iron and heat-shrink for the supply wiring in the base
- A 3D printer with a 256 mm bed. The parts are laid out for a Bambu Lab P1S
Files you’ll need: the STLs, the firmware and the Bambu Studio print project. The repository goes public together with this article, and the link lands at the end of it.
Start here. Order the parts (section 6 has the shop table with prices), start both print plates while you wait (section 5 has the settings), wire the base before any assembly (step 1 in section 7 explains why it comes first, section 8 has the wiring itself), then build steps 1 to 16 (section 7). When something misbehaves, troubleshooting is section 11, and the safety habits at the top of section 7 are worth the two minutes before the first power-up.
1. Why build a 3D printed robot arm from scratch (again)?
OmArm Zero was supposed to be a one-off: one 3D printed robot arm, built to see whether it could be done cheaply. Five joints plus a gripper, 303mm reach, 150g payload, about $50 in parts, controlled from a web browser served by an ESP32. It ended up carrying a four-part series through URDF export, ROS 2 motion planning and computer vision. By the end of it I had a long list of things I would do differently.
The list wasn’t really about Zero, though. It was about what I wanted to do next, which was to stop driving the arm myself.
An arm that follows sliders can be soft, slow and approximate, because a human is in the loop correcting it. An arm that takes an instruction like “pick up the red one” and executes it unsupervised cannot be any of those things. Section 3 makes that argument properly. It’s the reason a build guide for a hobby arm spends its first third on bearings and torque budgets.
So the brief for One wrote itself.
It has to lift 200g all day and 250g when asked. Not as a stunt at one lucky pose, but anywhere inside a normal desk working radius, without a servo getting hot. Zero managed 150g at full stretch with a spring helping the shoulder, and that was its ceiling.
It has to have six joints, not five. Five joints let you put the gripper somewhere. Six let you put it somewhere from a chosen direction, and that difference decides whether Parts 3 to 5 of this series work at all. A camera that finds an object also tells you how it’s oriented. A planner that can’t act on that is guessing.
Every joint that carries a moment gets a bearing. A moment is a twisting load, force times lever arm, and it is the currency this whole post trades in. This requirement shapes more of the design than any other one on the list.
On Zero the base was a printed platform screwed to a servo horn, with one small 686ZZ bearing steadying the shaft. When the arm reaches out, gravity doesn’t just try to rotate the base, it tries to tip it, and that tipping moment ran through the horn into the servo’s gear train. At 535g of arm it was acceptable. Scale the arm up and it stops being acceptable. The moment grows with both weight and reach, and a servo output shaft is built to deliver torque, not to hold a machine up.
So the requirement for One is blunt. The servo delivers torque, the bearings carry everything else. A 51110 thrust bearing takes the weight of the arm at the base and a 6806ZZ radial bearing takes the tipping. Every pitch joint gets a fixed-loose bearing arrangement, so the printed fork is supported on both sides instead of hanging off one servo spline. Section 5 walks through each one.
The difference shows up before anything is powered on. Rock a horn-mounted arm by the wrist and the gear train gives way, taking up its backlash. Press down on this one at full stretch and the load runs into a 70mm ring of bearing steel. Section 10 turns that into a check you can actually run, including which direction to push and which one tells you nothing.
One thing carries over from Zero unchanged: every component gets chosen by calculation first and catalog second. You’ll see the torque analysis before the parts list, the payload table before the payload test, and every payload claim tied to a working radius, because a payload number without a radius is marketing.
This is the first post in a five-part series; section 3 maps the other four. As with Zero, the arm was designed for all of it from the start, which is why the serial protocol, the joint conventions and even the shape of the workspace look the way they do.
2. What the arm can already do

The list below describes what the finished build does, and every item on it explains why some part of the mechanics looks the way it does.
Motion that doesn’t lurch
Every point-to-point move runs a coordinated quintic profile, a fifth-order position curve chosen so that speed and acceleration both start and end at zero. All seven joints share one duration, so they start together and arrive together, and the profile has zero velocity و zero acceleration at both ends. Nothing jerks at the start and nothing swings at the target. That matters more on a 485mm arm than on a small one, because a lurch at the shoulder becomes a visible whip at the fingertips.
Dragging a slider is a different problem, so it gets a different solution. The target keeps moving while the arm chases it, which makes planning a trajectory pointless. Each joint instead follows with limited acceleration, braking early enough that it physically cannot overshoot. Both paths respect the same velocity and acceleration ceilings, and both are bounded per control tick, so a stalled WiFi request or a slow HTTP call can never turn into one giant step. The motion core is plain math with no Arduino dependencies, so it compiles and runs on a PC against the same header the ESP32 executes.

A safety model built for an arm that can hurt you
This arm has no position feedback, so after a reset it does not know where it is, and the firmware treats that as a safety problem rather than a detail. Nothing moves on boot. A lost position has to be confirmed by a human before the servos come back. The first move after any boot is stretched to at least four seconds, and the emergency stop holds until you release it, instead of releasing itself. Section 8 walks through the whole model, including the one reset case that surprises people: a firmware upload counts as a power loss.
Resolution finer than the servo itself
The six joint servos run from a PCA9685 at 250 Hz, five times the classic servo rate. One PWM count (PWM, pulse-width modulation: the width of a repeating pulse tells the servo its target angle) is then 0.086 degrees at the joint, against the DS3240’s own dead band of 0.27 degrees, the smallest command change a servo physically responds to. The accuracy limit is no longer the electronics, it is the servo. That is where you want it, and it gives the repeatability check in section 10 something to mean.

Two firmwares, one wiring
The same board, the same channels and the same conventions carry two builds.
إن web app firmware brings up its own WiFi access point, serves a control interface with seven joint sliders, a speed control, a home command, an emergency stop and a small sequence editor that records poses and plays them back with a configurable delay. Sequences export and import as files, so a demo you set up once survives a reflash. Nothing is installed on your phone or laptop. The interface lives in the ESP32.
إن ROS 2 serial firmware drops WiFi, the web app and the filesystem, which keeps it small and keeps anything that could disturb servo timing out of the way. It adds what a planner needs: trajectory following, where a whole path is loaded and validated completely before anything moves. The check covers joint ranges, the speed ceiling, the timing, and whether the trajectory even starts where the arm is currently standing, within five degrees. Only then does anything execute. If it fails validation you get a refusal with a reason instead of a surprise movement. That validation-before-execution rule is the part I’m most pleased with, and it comes straight from watching the predecessor’s ROS bridge quietly execute only the last waypoint of a plan.
Switching between the two is a reflash. The stored arm position survives, so the arm doesn’t move when you change its brain. Both compile clean on the current ESP32 core: the web-app build fills 80 percent of the 1.25 MB app partition, the serial bridge 26.
3. Where this series is going
This arm is the vehicle for a five-part series, in the order a real project runs. Every part ends with something that runs.

| الجزء | What gets built | What you learn |
|---|---|---|
| 1 (this post) | The physical arm: requirements, statics, CAD, BOM, printing, assembly, wiring, calibration, payload test | Requirements engineering, torque and payload analysis from a free-body diagram, bearing selection, design for FDM, component selection with numbers behind it, forward kinematics and DH parameters, an ESP32 control stack with a web interface |
| 2 | The digital twin | Exporting a URDF from the CAD, RViz visualisation, Gazebo simulation, driving the twin and the real arm from a game controller |
| 3 | Motion planning | MoveIt 2, inverse kinematics (working out joint angles from a desired hand position) on a non-spherical wrist, trajectory planning, collision awareness, and executing planned paths on real hardware through the serial bridge |
| 4 | Eyes | Camera integration, hand-eye calibration, and pick and place of a detected object |
| 5 | Judgment | Grasping without markers: the arm finds arbitrary objects and picks the class you name |
Part 1 does not need an arm built to this standard. Parts 4 and 5 do. Picture the end state: you say “pick up the red one.” A camera looks at the table, a model decides which object that is, a planner works out how to approach it, and the arm does it. Swap the model for a language model and the same sentence can come from speech, from a chat window, or from another program. The arm becomes the hand and something else does the thinking.

That end state is unforgiving in a way slider control never is. Object detection returns positions in millimeters, so the arm has to be repeatable in millimeters. An autonomous cycle repeats a motion far more often than a human demo ever does, and it finds every edge case you didn’t test, so the failure behaviour has to be safe by construction rather than by attention. That is what the requirements in section 4 are for: bearings under the load paths, six joints so the approach direction is part of the grasp, 200 grams nominal and 250 maximum at a radius where objects actually sit, a motion core that cannot overshoot, and one angle convention from CAD to servo so the digital twin aims at the machine instead of at a fiction.
None of that AI exists yet. Parts 4 and 5 build it, and I’ll write up what happens when I get there, including the parts that don’t work. The machine underneath has to be ready first, and building the mechanics for the demanding case is a lot cheaper than rebuilding an arm that turned out to be too soft for it. So One exists instead of a bigger Zero.
4. System requirements and engineering specification
Before I drew a single part I wrote down what this arm has to do. For One the list is shorter than Zero’s and considerably harder.
Functional requirements
F1. Payload: 200g nominal and 250g maximum, both sustained at a 300mm working radius. Nominal is the duty the arm lives at; maximum is what it must still hold without leaving the thermal rule. Sustained means the servos hold it indefinitely without cooking, at the worst joint configuration that reaches that radius, not at a hand-picked pose.
F2. Reach: at least 480mm horizontally from the base axis to the closed gripper. Zero reached 303mm, so this is around 60 percent more, enough to cover a desk instead of a corner of it.
F3. Six positioning joints plus a parallel-jaw gripper. Base yaw, shoulder pitch, elbow pitch, forearm roll, wrist pitch, tool roll. With six the planner picks the approach direction as well as the target point.
F4. Bearings, not servo horns, carry the moments. No joint may transfer its bending load through the servo output shaft alone. Where the moment is genuinely negligible, show the number instead of assuming it.
F5. Control: WiFi web GUI for standalone use, USB serial for ROS 2. Same dual interface as Zero, same wiring for both.
F6. Angle convention: every joint 0 to 180 degrees, CAD angle equals servo angle. No offset tables, no per-joint sign flips. It sounds like bookkeeping, and it makes the digital twin in Part 2 line up with the hardware on the first try.
F7. Accuracy floor set by the servo, not the structure. The DS3240’s dead band is 3 microseconds, which over its 500 to 2500 microsecond range works out to 0.27 degrees. The mechanics must not add slop on top of that.
Non-functional requirements
One servo model for all six joints. Zero mixed MG996R and SG90 to save weight where torque wasn’t needed. One deliberately doesn’t: one spare covers any joint, one calibration procedure covers the arm, one mounting geometry repeats through the whole CAD. The cost is carrying shoulder-class torque out at the wrist, and the torque budget below makes that affordable.
Torque margin: the worst-case torque at every joint, at the 250g maximum, stays at or below 63 percent of stall, computed with conservative masses and with no help from the shoulder spring. This single rule drives the servo choice.
Power: one 6V rail, 10A or more, with bulk capacitance for transients. 6V is the standard rail rather than an upgrade, because the 250g maximum needs the torque the DS3240 only makes there, and the SG90 gripper caps the voltage at exactly that value, as section 6 explains.
Printability: every structural part on a Bambu Lab P1S (256mm bed) in PLA. Eighteen printed bodies, 23 pieces on the plate.
Assembly with a normal maker toolbox. No CNC, no laser cutting, no press tooling.
Kinematic layout
Six revolute joints in series. The proportions matter as much as the count, so here they are measured along the chain:

J1, base yaw. Rotates the whole arm about the vertical axis, 0 to 180 degrees. Gravity produces no torque about a vertical axis, which sounds easy until you notice what it does produce, a tipping moment that grows with reach. The fix for that is a bearing, not a stronger servo.
J2, shoulder pitch. Sits 91.6mm above the table, 57.79mm along the chain from J1. This joint carries the arm. Everything beyond it acts on a lever up to half a meter long, and the analysis below confirms it is the critical joint of the machine.
J3, elbow pitch. 165.68mm from the shoulder, which is the upper arm span. Carries the forearm, wrist and gripper.
J4, forearm roll. 80.29mm past the elbow. Rotates the entire wrist assembly about the forearm axis.
J5, wrist pitch. 59.85mm further out. Pitches the gripper up and down.
J6, tool roll. Another 80.29mm out. Rotates the gripper about its own axis.
Gripper. Gear-driven parallel jaws, grasp point 120.06mm beyond the tool flange. Every reach and payload number in this post is quoted at that grasp point, not at the flange.
One geometric detail doesn’t show up in a photo. The two roll axes are tilted, not parallel to the forearm. Section 5 explains what that costs and why it’s free for this project.
Denavit-Hartenberg parameters
New to robotics? Skip this table. The takeaway is that four numbers per joint fully describe the arm’s geometry, and that this table makes the digital twin in Part 2 match the hardware.
a is the link length, alpha the twist between joint axes, d the offset along the previous axis, theta the joint variable. The offsets in the last column are the difference between the DH zero and the servo zero, and they are there because the arm was drawn first and parameterised afterwards. They do not break F6: the CAD angle still equals the servo angle, and these numbers only relate that shared angle to the convention’s own zero.
| i | a (mm) | alpha (deg) | d (mm) | theta |
|---|---|---|---|---|
| 1 | 0.00 | −90.00 | 0.00 | q1 |
| 2 | 165.68 | 180.00 | −30.63 | q2 − 152.66° |
| 3 | 0.50 | −90.00 | −30.63 | q3 + 107.12° |
| 4 | 0.00 | −90.00 | 125.46 | q4 + 180.00° |
| 5 | 0.50 | −90.00 | 0.27 | q5 + 90.00° |
| 6 | 0.00 | 0.00 | 74.21 | q6 + 90.00° |
Two entries would not have survived a hand-drawn table. The 0.50mm figures in the a column are real. The CAD carries a half-millimeter lateral offset between the pitch axes and the roll axes, and a hand-drawn table would round that away. And the alpha column holds clean multiples of 90 degrees despite the 10 degree roll tilt, because the tilt lies in the plane of the common normal and the convention folds it into the offsets instead of the twist.
One confession belongs next to this table. An earlier version of it was wrong, sat in this post for two weeks, and passed a check that could not have caught it, because the check compared two formulations that both bypassed the table. The bug shortened the chain by both 80.29 mm forearm segments: the sum of its own columns came to 327 mm out to the flange instead of 399, which caps the grasp point at 447 mm, and the arm measurably reaches 485. The check that runs now builds a forward kinematics out of the printed rows and matches it against the URDF over 2000 random configurations to 0.000 µm, and the script refuses to print a table whose geometric bound falls short of the measured reach. The full post-mortem, with the broken derivation and the fix, is in the engineering study in the repo.
Torque analysis
For a joint holding the arm against gravity, the worst-case static torque is
tau = sum over all links beyond the joint of ( m_link * g * horizontal_distance_to_axis )
evaluated at the worst pose that joint can be commanded into. On Zero I did this with four lumped masses and a calculator. One has six joints and twelve bodies, so instead of guessing the worst pose I let the CAD mass model compute it, scanning the joint space for each joint’s true worst case.

About those masses: they come from the CAD solids, and they contain two large errors that point in opposite directions. The model treats every printed part as solid material while the real parts are shell plus infill, so the plastic is over-counted, and it carries no servos, bearings, screws or wiring at all, so the hardware is missing entirely. After assembly the complete machine went on a kitchen scale: 1283 g, against the solid model’s 1284 g. That near-miss is not luck cancelling itself so much as two errors trading places, because the roughly 860 g of hardware the model ignores (servos, bearings, 86 screws, boards, wiring) almost exactly refills what shell-and-infill printing saves. Entered into the analysis script as MEASURED, the scale factor comes out at 0.999, every torque and payload figure below moves by a tenth of a percent, and the tables stand as printed.
Required torque per joint, with the arm carrying the F1 maximum of 250g at a 300mm radius. That is the load case the motors are sized against. Each joint is scanned in its own worst pose, so no joint is flattered by another joint’s geometry, and the first two columns are the two parts of the same sum:
| مشترك | Arm itself | Payload | M required (kg·cm) | Stall needed, M/0.63 | What it means |
|---|---|---|---|---|---|
| J1 base yaw | 0.00 | 0.00 | 0.00 | 0.0 | no gravity torque, but the largest bending moment on the arm |
| J2 shoulder | 16.10 | 7.50 | 23.60 | 37.5 | the critical joint, this row sizes the servo |
| J3 elbow | 6.21 | 7.99 | 14.20 | 22.5 | second worst, and now more payload than arm |
| J4 forearm roll | 1.46 | 4.85 | 6.31 | 10.0 | payload dominates from here out |
| J5 wrist pitch | 1.46 | 4.85 | 6.31 | 10.0 | same |
| J6 tool roll | 0.01 | 0.31 | 0.33 | 0.5 | negligible; section 5 comes back to this number |
The largest requirement on the whole arm is 37.5 kg·cm, and that single number decides two things at once. It picks the servo class, and it picks the rail voltage: the DS3240 delivers 36 kg·cm at 5 V and 41 kg·cm at 6 V, so 6 V is the standard rail on this arm because the 250g maximum needs it. At 5 V the arm still meets the 200g nominal case, which asks for 35.1. Two more things in that table are worth a second look. The ratio of arm to payload flips along the chain, from 68 percent structure at the shoulder to 23 percent at the mid-arm joints, which is why lightening the forearm buys more payload than a bigger servo does. And the shoulder needs 23.60 kg·cm where the elbow needs 14.20, so one joint decides the whole machine. Because that one number picks the servo class, it is worth showing that it really is conservative rather than asserting it. The model over-counts the plastic and carries no hardware at all, so I rebuilt the shoulder moment from components. Printed structure beyond J2: 248 g, from PLA at 1.24 g/cm3 and a 24 percent fill calibrated against the slicer’s measured 421 g. Hardware the model does not carry: five DS3240 at 58 g each and the SG90, at the joints they actually sit in, out to 365 mm; three 695ZZ at 2.31 g; the three M5 axles; 40 screws; five horns; eight brass inserts; the spring; and 1.6 m of servo cable. That is 419 g, and the sum comes to 15.13 kg·cm against the model’s 16.10 for the same load case. The build-up accounts for 1199 of the 1283 g on the scale, and if every unaccounted gram is placed beyond the shoulder instead of in the base the moment still lands at or below the model. So the model never underestimates, and the 23.60 kg·cm is a ceiling rather than a best guess. That is why 250g is a design target instead of a hope.
Payload derivation: where the 200g and the 250g come from
At stall torque the motor stops dead and draws its maximum 3.1A. Park a servo anywhere near that and it cooks itself in minutes, so payload is never specified against stall. Testing on Zero settled the ceiling at 63 percent of stall for sustained load, and that rule carries over unchanged.
Payload then falls out pose by pose: take the torque headroom left at each joint, divide by the payload’s lever arm about that joint, and the smallest result across all six joints sets what the arm can hold in that pose. Do that across the workspace and you get the honest form of a payload spec, which is a table against working radius instead of a single number:
| Working radius | 6 V sustained | 6 V at stall | 5 V sustained | 5 V at stall |
|---|---|---|---|---|
| 250 mm | 407 g | 1013 g | 281 g | 813 g |
| 300 mm, requirement F1 | 325 g | 830 g | 219 g | 664 g |
| 303 mm (Zero’s full reach) | 321 g | 821 g | 217 g | 656 g |
| 350 mm | 268 g | 701 g | 178 g | 558 g |
| 370 mm, the 250 g maximum | 252 g | 661 g | 167 g | 526 g |
| 400 mm | 225 g | 605 g | 146 g | 480 g |
| 435 mm, the 200 g nominal | 202 g | 551 g | 129 g | 436 g |
| 485 mm (full stretch) | 175 g | 488 g | 110 g | 385 g |

The 6 V columns are the ones that count, because 6 V is the standard rail on this arm. The 5 V columns are there because a lot of benches have a 5 V supply lying around, and they show what it costs: the same servo drops from 41 to 36 kg·cm, and at 300 mm the envelope falls from 325 g to 219 g. Enough for the 200 g nominal case, not for the 250 g maximum.
So requirement F1 checks out with room to spare. At a 300mm working radius the arm sustains 325g on the standard rail, and holding the 250g maximum there loads the worst joint to 57.6 percent of stall, comfortably under the 63 percent line. J2 is the binding joint in every loaded pose. Scanned with each joint in its own worst loaded configuration at the maximum, J3 comes second at 34.6 percent, the two mid-arm joints reach 15.4, and the tool roll stays under 1. The 250g maximum runs out at 370mm and the 200g nominal case at 435mm. That’s the honest edge of the spec, and anything beyond it belongs to the lighter rows of the table.
The same table answers two questions I had about Zero. At Zero’s full reach of 303mm this arm sustains 321g where Zero managed 150g, so more than twice the payload at the same radius. And Zero’s entire rated payload of 150g stays available past 485mm, which is further than Zero could reach at all.
The stall columns are there for context, not for use. At a 300mm radius the arm runs out of torque completely at 830g, and that figure is the wall the geometry hits rather than a payload you can use. Getting close to it is a redesign conversation.
Workspace and reach
The grasp point reaches 485mm horizontally from the base axis, and 487mm measured from the shoulder, which sits 91.6mm above the table and 30.63mm off the base axis. Because J1 sweeps 0 to 180 degrees and not a full circle, the workspace is a forward-facing half-shell of roughly 487mm outer radius.
That 487 was briefly 516, from a scan that froze the shoulder at zero while J1 turned. The corrected number cleared the same sanity check as the table above: a distance cannot exceed the chain that spans it.

Against Zero: 303 to 485mm of reach, 60 percent more, and since volume goes with the cube of that, about four times the reachable space. The half-shell is deliberate. A front-facing workspace gives the camera in Parts 4 and 5 one clean scene with the arm always arriving from a known side.
Power budget
Seven servos on one rail. From the DS3240 datasheet: 4mA idle per joint servo, 3.1A stall at 5V.
| Scenario | Current at 5 V |
|---|---|
| Idle, all joints holding | 24 mA plus electronics |
| Typical coordinated motion | about 4.3 A |
| Worst case: six stalled joints plus gripper | 19.3 A |
A 5V 10A supply covers every realistic case. The 19.3A figure needs all six joints mechanically blocked at once, which is a crash, not an operating point, and the firmware’s job is to never leave the servos sitting there. A 1000 to 2200 microfarad electrolytic across the rail carries the start-up and direction-change transients. If you want the pathological case covered on paper too, a 20A supply does it.
The supply side has two rules I would not bend. The rail is 6V, the ceiling the SG90 gripper sets; section 6 has the whole trade. And servo power never comes from the ESP32’s USB pin. Zero’s post said the same thing, it’s still true, and it still kills boards.
A third rule I earned during this build: check the rail voltage twice before you plug anything in. I fed the servo rail 8 volts once, out of a bench supply that was still set for the previous project. The arm twitched by itself, the ESP32 got worryingly hot, and my PC shut its USB port down to protect itself. Everything survived, which says more about luck than about margins. An adjustable supply remembers whatever you last used it for; set it, read the display, and only then plug in the arm.
5. Mechanical design and CAD

OmArm One is drawn in Autodesk Inventor, for the least romantic of reasons: it is the CAD I am fastest in, and this arm needed every hour on the mechanics rather than on learning a new tool. Everything in this post comes out of that one assembly: the exploded animations in section 7, the drawing and its balloon numbers in section 6, and the STLs on both plates. Part 2 needs an exact digital twin of this geometry, so a part that does not export cleanly gets paid for one section later.
Eighteen printed PLA bodies, seven servos, and a set of ball bearings that decide how this arm ages. The brief for every part was the same: put material where the load path is, let the servo do torque and nothing else, and never ask layer adhesion to hold the machine together.


The base: one servo, two bearings, three jobs
The base does three jobs at the same time: it rotates the arm, it carries the arm’s weight, and it resists the arm’s tipping moment. On Zero a single servo horn did all three. Here they are split apart.
The rotating platform sits on a 51110 axial thrust bearing, 50 x 70 x 14mm, a flat ring of balls in a cage running between two hardened washers. Thrust bearings are built for exactly one thing, load pressing straight down along the axis. The whole weight of the arm, plus whatever it’s holding, goes through this ring and into the base housing. It also does something less obvious but just as useful. It sets the height of the platform precisely and holds it there while it turns.
Inside that ring sits a 6806ZZ radial ball bearing, 30 x 42 x 7mm, wrapped around the hub of the rotating platform. Radial bearings take load perpendicular to the axis, which here is the tipping component. An arm reaching out to 485mm tries to lever the platform sideways. Because the 6806 is a thin-section bearing with a large 42mm outer diameter, it resists tilt across a wide base instead of a narrow one, which actually keeps the platform flat.
What’s left for the servo is rotation. Its horn bolts to the platform hub through four screws, and the only load reaching the gear train is the torque needed to turn the arm. The base bearing pair separates this arm from a printed toy.
There’s a bonus in the geometry. Because the thrust ring is 70mm across, it defines a wide, stable support circle underneath the arm, far wider than a servo horn’s 20-odd millimeters. Tipping loads that used to concentrate on a spline are spread across a 70mm circle of steel.



The base housing has a second job as the electronics enclosure. Both boards mount on the inside wall, the switch and the barrel jack sit in the outer face, and the top surface is vented over the driver board. That is why the base is shaped like a box with a cylinder growing out of it rather than a simple disc, and why the only thing leaving the finished arm is one cable.
Pitch joints: fixed-loose bearing arrangements at J2, J3 and J5
The three pitch joints carry bending load while they rotate, and all three use the same arrangement, which mechanical engineers call fixed-loose (Fest-Los-Lagerung): one side of the shaft is located rigidly, the other side is supported but free to move a fraction of a millimeter along the axis.
In practice each pitch joint is a printed fork straddling the next link:
- The fixed side is the servo and its horn. The 25T horn (25T: a spline with 25 teeth, the de-facto standard on servos of this class) is bolted to one arm of the fork while the servo body sits in the mating part, so the joint is driven through the spline. This side transmits the torque and locates the joint axially. Which of the two parts holds the servo body changes along the arm: at the shoulder and the elbow both servos live in the upper arm and their horns bolt outward from it, which is why step 5 fits two servos to that one link before it goes anywhere near the machine.
- The loose side is a 695ZZ miniature ball bearing, 5 x 13 x 4mm, pressed into the opposite fork arm. An M5 bolt runs through the bearing’s inner ring with a printed sleeve as the spacer, and an M5 nut clamps the stack against the link. The link now turns on a bearing instead of rubbing in a printed hole, and the fork is supported on both sides.
Why not two rigid bearings? Because two rigid supports on one shaft fight each other. Printed parts change dimensions with temperature and humidity, the print tolerance across a 60mm fork is a few tenths of a millimeter, and if both sides are locked the shaft ends up preloaded in a way nobody designed. It binds, the servo works against it, and you lose torque to friction that should be going into payload. The fixed-loose arrangement takes the bending on both sides while letting the axial length float. Every machine tool spindle does the same thing, and here it is scaled down to a printed arm.

The sleeve is worth a note of its own, because it’s the part most likely to be improvised badly. Its length sets how tightly the fork is clamped against the link. Too short and you squeeze the fork arms inward until the joint stiffens; too long and the link rattles. Print or turn it to the CAD length and you get a joint that spins freely under its own weight but has no measurable side play. Both sleeves are printed parts: Ø 8.9 mm outside, 5.2 mm bore, 9.0 mm long at the shoulder (sleeve_1) and 8.0 mm at the other two pitch joints (sleeve_2). Print them lying down so the bore stays round, or turn them from aluminium if you have a lathe and want the clamp faces dead square.
The shoulder spring
There’s an extension spring at the shoulder, mounted between two printed lugs, one on the rotating base platform and one on the upper arm, with a pin through each hooked end.
Zero had a spring too, and it did a different job there. On Zero the spring was needed. The shoulder servo sat at the edge of its class, and the spring kept the arm inside its torque budget. On One it’s a safety factor. Every torque and payload number in section 4 was computed with no spring at all, and the shoulder still comes out at 57.6 percent of stall with the 250g maximum in the jaws. So the spring isn’t holding the arm up, it’s buying reserve: less load on the servo, less heat on long holds, less current at full stretch, and a gentler failure if the servo ever loses power while the arm is extended. Take it off and the published specs still hold. That’s the difference between a crutch and a margin.
I measured it rather than guessed it, with a luggage scale and a steel rule. At rest the spring’s end sat at the 30mm mark and the scale read zero. Pulled out to the 60mm mark, so 30mm of extension, the scale read 1.33kg.
That’s 13.05 newtons at 30mm of extension, and 13 N at 30mm is the number to buy against.
Turning it into a spring rate takes one more step that’s easy to get wrong. Extension springs are wound with initial tension. The coils are pressed together and the spring doesn’t start opening until you exceed a preload force F0, so the load line is F = F0 + k·s rather than F = k·s. Divide 13.05N by 30mm and you get 0.435 N/mm, but that value has the preload smeared into it, which makes it an upper bound. The true rate lands between 0.33 and 0.44 N/mm depending on the preload, and the closest catalogue spring I could find (0.8mm wire, 8.8mm outer diameter, 32.3mm body inside the hooks, which is 39.5mm hole to hole) publishes 0.333 N/mm with 1.55N of initial tension. That works out to 11.5N at 30mm, within 12 percent of the spring on my arm.
If you want the exact rate rather than the range, take two loaded readings instead of one: pull to 15mm extension, note the force, pull to 30mm, note it again. The preload cancels in the difference and you get k directly. The full measurement, the arithmetic and the catalogue comparison are in 01_repo/kinematics/spring-rate-measurement.md in the repo.
Buying a substitute. Order by behaviour, not by appearance. The spring must pull 11 to 15 newtons at 30mm of extension and measure 38 to 42mm hole-to-hole at rest, with hooks that take the pin and at least 35mm of usable travel. Wire around 0.8 to 1.0mm, outer diameter 8.5 to 10mm. That tolerance is deliberately wide, because this spring is margin rather than necessity, and a spring 20 percent softer costs a little assist and nothing else. What you want to avoid is a spring that looks identical and is twice as stiff. Those exist in the same 9mm outer diameter and the same wire gauge, they just have fewer coils, and a spring that stiff fights the servo on the way down.
The spring stays in the final build. Its line of action runs about 36 mm from the J2 axis, measured off the CAD front elevation at platform scale, so the measured 13 N is worth roughly 4.8 kg·cm of relief at the shoulder at that geometry. How much of it survives across the joint range is a different question, because the assist changes with the shoulder angle and I have not measured that curve. Which is exactly why no torque figure in section 4 includes it. The 250 g maximum holds without the spring; everything the spring gives is on top.

Roll joints: J4 and J6
The two roll joints are coaxial instead of forked. The servo turns the next segment about its own axis, so the load path is different. Here the bending moment mostly stays inside the segment instead of trying to pry a fork open.
J4, the forearm roll, gets a 6806ZZ radial bearing in addition to its servo horn. It sits far enough out that the wrist and gripper still generate a real moment about it, and the bearing keeps that moment out of the servo.
J6, the tool roll, was drawn for the same 6806ZZ, and in the design as it stands it runs on the servo horn alone. That’s a deliberate exception, and section 4‘s torque table explains it. J6’s worst-case gravity torque is 0.01 kg·cm, which is roughly a thousandth of what the shoulder sees, and the mass hanging off it is one printed gripper. There is no moment there worth putting a bearing under. This is what F4 meant by “where the moment is genuinely negligible, show the number”. The bearing seat stays in the CAD in case someone hangs a heavier tool off the flange.
The tilted wrist, and what it costs
Both roll axes, J4 and J6, sit 10 degrees off the forearm centerline. The consequence is larger than the angle. A wrist whose three axes don’t meet at a single point is not a spherical wrist, and without a spherical wrist there’s no textbook closed-form inverse kinematics. Every pose has to be solved numerically.
For a classical robotics demo that’s a defect. For this series it’s free, because Part 3 hands the arm to MoveIt 2, which solves IK numerically anyway and never asks whether your wrist is spherical. I’d rather keep the geometry the mechanics wanted than distort the mechanics to please a solver I’m not going to use.
The tilt is packaging, and the section drawing shows it plainly: the roll servos nest inside their segments, and squaring the output axis to the forearm would have cost either a fatter segment or a servo pod hanging outside the envelope. The axis follows the servo instead, and the 10 degrees ride along.
تجميع القابض
The gripper keeps Zero’s concept: one servo drives a gear pair, the gear pair swings two fingers, and both fingers close symmetrically toward the centerline. Parallel jaws matter more than they sound like they should, because an object that gets pushed sideways during the grasp ends up somewhere the camera didn’t predict, and Parts 4 and 5 depend on it not doing that.
Eight printed pieces ride on the gripper base: two gears, four linkage arms and two jaws, and the arms are pinned at both ends by M3 screws rather than by pins. The grasp point sits 120.06mm beyond the tool flange, and every reach and payload figure in this post is quoted there.

Three things changed against Zero’s gripper. The linkage joints now run on screws in M3 threaded inserts instead of screws against nuts, which ends the slow self-loosening Zero’s jaw developed and takes the play out of the linkage. The gear pair grew wider, so the teeth stay fully meshed under load instead of riding up. And the gripper meets the tool flange on a four-hole bolt circle now, so a different gripper, or a different tool entirely, mounts to the arm without touching the wrist.
Design for manufacturing: what a 3D printed robot arm asks of the printer
All of it prints in PLA on a Bambu Lab P1S, across two plates.
Twelve is the count of bodies the kinematic model moves, named the way the URDF names them, not the count of parts on the plate. Sleeves, the two spring pins, the four jaw linkage arms and two covers are printed as well; none of them appear here because nothing in the model rotates about them. Counted the way a slicer counts, the plate holds 23 pieces from 18 distinct bodies, and the full list, keyed to the drawing’s balloon numbers, is in section 6.
| Body in the model | الكمية | دور |
|---|---|---|
| base_link | 1 | Base housing: servo pocket, bearing seats, electronics bay |
| base_link_upper | 1 | Rotating platform on the bearings, carries the shoulder fork |
| link_1 | 1 | Upper arm, shoulder to elbow |
| link_2 | 1 | Forearm segment, elbow to forearm roll |
| link_3 | 1 | Forearm segment, forearm roll to wrist pitch |
| link_4 | 1 | Wrist segment, wrist pitch to tool roll |
| tool_link | 1 | Tool flange, carries the gripper |
| قاعدة_القابض | 1 | Gripper housing |
| gripper_gear | 2 | Gear pair, one driving, one driven |
| gripper_finger | 2 | Parallel jaws |
| Total | 12 |
Printing this arm differs from a typical print job in three places. The bearing seats need to come out at size, so run a dimensional test before you print the base. A 42mm bore that shrinks to 41.7 will not accept a 6806ZZ, and a bore that prints 0.3 large gives you a bearing that spins in its own seat. The fork arms need enough perimeters to stay stiff, because a fork that flexes throws away the benefit of the loose bearing. And the print orientation of the arm links should put the layer lines along the span, not across it, so bending load runs along the material instead of peeling it apart.
The figures below come out of the Bambu Studio project for this arm. Both plates run in Generic PLA on a P1S with a 0.4 mm nozzle, using the stock 0.20 mm Standard @BBL X1C profile, which is what these two plates were sliced with even though the printer is a P1S.
| Plate | Model filament | Support | Total | Print time |
|---|---|---|---|---|
| 1, the large parts | 85.77 m / 255.81 g | 7.69 m / 22.94 g | 93.46 m / 278.75 g | 9 h 07 min |
| 2, the small parts | 55.40 m / 165.22 g | 1.55 m / 4.63 g | 56.95 m / 169.85 g | 5 h 59 min |
| 421.03 g | 27.57 g | 448.60 g | 15 h 06 min |
There are two settings I would copy rather than guess at. Supports are tree, style Tree Slim, threshold 45°, with support critical regions only و remove small overhangs both on, which keeps the tree out of the bearing bores and the servo pockets. And the top Z distance is 0.2 mm, exactly one layer, so supported faces come off cleanly instead of fusing. Raft is off, and the initial layer runs at 90 percent density.
The Bambu Studio project with both plates is in the repo at cad/3d_print/OmArm-One_print-plates.3mf, so the plate split above is reproducible rather than described.

Those filament figures let me replace an estimate with a build-up. The analysis script, working from the URDF alone, guessed the finished arm at 0.85 to 1.05 kg against the solid model’s 1.284 kg. With the printed mass now coming off the slicer, the arm adds up like this:
| Mass | Where it comes from | |
|---|---|---|
| Printed parts | 421 g | the slicer, model filament only |
| الماكينات | 357 g | 6 × 58 g DS3240 plus a 9 g SG90, vendor spec |
| Bearings | 221 g | 51110 at 160 g, two 6806ZZ at 27 g, three 695ZZ at 2.31 g, catalogue |
| Fasteners, horns, inserts, spring, boards, wiring | ~200 g | itemised from the parts list |
| ~1.20 kg |
So the built-up mass sits about 6 percent below the weighed arm, and the remainder is unlisted small hardware in the base. The payload numbers in section 4 stay conservative either way, because they were computed with the heavier model.
Weighed after assembly: 1283 g complete, entered as MEASURED; the recomputed tables shift by a tenth of a percent and stand as printed.

6. Bill of materials and component selection

cad/drawings/.Complete parts list
The first column is the balloon number in the drawing above, taken straight from the Inventor assembly. The numbers are not in sequence, because Inventor numbers parts in the order they entered the assembly and I did not renumber them to look tidy. Point at a balloon, find the row. Position 23 is missing on purpose: the export lists it as fifteen Document entries, which is Inventor bookkeeping rather than anything you can hold.
The prices in the shop table below are street prices as of August 2026, rounded and marked as approximate. They are what the parts cost to order today, not what my own build cost me: mine came together over months and out of two boxes of leftovers, so an order total from it would be fiction.
Printed parts. Eighteen distinct bodies, 23 pieces on the plate.
| # | الجزء | الكمية | دور |
|---|---|---|---|
| 1 | base_link | 1 | Base housing: servo pocket, bearing seats, electronics bay |
| 2 | base_link_upper | 1 | Rotating platform on the bearings, carries the shoulder fork |
| 39 | base_link_AKL | 1 | Base cover plate |
| 18 | arm_link_1_2 | 1 | Upper arm, shoulder to elbow |
| 5 | arm_link_22 | 2 | One body printed twice, which is why the two 80.29 mm forearm segments in section 4 come out identical |
| 6 | arm_link_33 | 1 | Forearm segment, forearm roll to wrist pitch |
| 7 | arm_link_tool | 1 | Tool flange, carries the gripper |
| 36 | arm_link_cover | 1 | Servo bay cover on the arm |
| 8 | قاعدة_القابض | 1 | Gripper housing |
| 26 | gripper_gear_left | 1 | Driving gear |
| 27 | gripper_gear_right | 1 | Driven gear |
| 9 | gripper_link | 2 | Upper links of the jaw linkage |
| 24 | gripper_link_bottom | 2 | Lower links of the jaw linkage |
| 10 | gripper_part_left | 1 | Left jaw |
| 11 | gripper_part_right | 1 | Right jaw |
| 13 | sleeve_1 | 1 | Bearing spacer, one pitch joint. Ø 8.9 x 9.0 mm, 5.2 mm bore, measured from the STL |
| 17 | sleeve_2 | 2 | Bearing spacer, the other two pitch joints. Ø 8.9 x 8.0 mm, same 5.2 mm bore, per the drawing |
| 33 | pin | 2 | The two pins that hold the shoulder spring in its lugs |
| Total | 23 |
Bought, mechanical.
| # | الجزء | الكمية | Spec | Source | Price |
|---|---|---|---|---|---|
| 3 | Joint servo J1–J6 | 6 | DSSERVO DS3240-180, 36 kg·cm @ 5 V, 40 x 20 x 40.5 mm, 25T spline. Read the note below about what the CAD calls this part | Buy | see shop table |
| 4 | Servo horn, 25T | 6 | Ships with the servo | with servo | included |
| 38 | 51110 axial thrust bearing | 1 | 50 x 70 x 14 mm, chrome steel, with washer. In the base, carries the arm’s weight | Buy | see shop table |
| 20 | 6806ZZ radial ball bearing | 2 | 30 x 42 x 7 mm. One in the base (tipping moment), one at J4 forearm roll | Buy | see shop table |
| 12 | 695ZZ miniature ball bearing | 3 | 5 x 13 x 4 mm. Loose side of the pitch joints J2, J3, J5. The drawing calls it 619/5, which is the same bearing under its ISO designation | Buy | see shop table |
| 34 | Extension spring | 1 | Shoulder assist: 13 N at 30 mm extension, 40 mm hole-to-hole at rest, wire 0.8–1.0 mm, outer Ø 8.5–10 mm (measured, see section 5) | Buy | see shop table |
| 29 | جهاز مؤازر صغير SG90 | 1 | Gripper drive, 9 g, analog | Buy | see shop table |
| 30 | SG90 horn | 1 | Ships with the servo | with servo | included |
Fasteners. 86 pieces, and this is the part of the list I could not have written before the drawing existed.
| # | Fastener | الكمية | Where |
|---|---|---|---|
| 15 | Hex bolt M5 x 20, full thread | 2 | Pitch-joint bearing axles |
| 16 | Hex bolt M5 x 25, full thread | 1 | The third pitch-joint axle |
| 14 | Hex nut M5 | 3 | One against each of the three bolts above |
| 22 | Self-tapping pan head, #2 x 3/8″ (≈ M2.6 x 9.5) | 24 | Six servos, four screws each |
| 21 | Self-tapping pan head, #1 x 3/8″ (≈ M2.2 x 9.5) | 26 | Printed housings |
| 35 | Self-tapping pan head, #1 x 1/4″ (≈ M2.2 x 6.4) | 8 | Printed housings, where the wall is thinner |
| 19 | Self-tapping ISO 7049 ST2.2 x 4.5 | 5 | Boards and covers |
| 25 | Socket head ISO 4762 M3 x 25 | 4 | Jaw linkage |
| 28 | Socket head ISO 4762 M3 x 16 | 4 | Jaw linkage |
| 32 | Socket head ISO 4762 M3 x 8 | 4 | |
| 44 | Socket head ISO 4762 M3 x 10 | 2 | |
| 37 | Pozidriv AS 1427 M3 x 8 | 2 | |
| 31 | Pan head EN ISO 7045 M1.6 x 5 | 1 | SG90 horn |
The three M5 rows are worth reading together. Three bolts, three nuts, three sleeves, three miniature bearings. That one-to-one match is what caught the bearing error described below.
Which screw goes where. Quantities and designations below come from the Inventor parts list; the locations come from the assembly animations in section 7, which is also where the step numbers point.
| Items | Fastener | Goes where | Step |
|---|---|---|---|
| 15, 16, 14 | 3 x M5 bolt + nut | Pitch-joint axles: the 25 mm bolt at the shoulder, the two 20 mm at elbow and wrist. The wrist bolt goes in at the end of step 10 and its nut at the start of step 11 | 6, 8, 10 |
| 22 | 24 x #2 self-tapping | Four at every DS3240. Six servos, four screws each, which is where all 24 go | 2, 5, 7, 11, 12 |
| 33, 34 | 2 pins + spring | The spring hangs between its two printed lugs on these pins | 6 |
| 19, 21, 35 | 39 cross-head pan heads in three lengths | Everything else printed: the two boards on the inner wall of the base, the servo horns to the links they drive (four per horn), and the base shell and its face. Match the length to the wall you are driving into rather than to a balloon number, and drive them slow: a self-tapper cuts its own thread in PLA and will strip it just as easily | 1, 4, 5, 6, 7, 8, 10, 12, 13 |
| 37 | 2 x Pozidriv M3 x 8 | Item 36, the dark cover strip on the upper arm | 5 |
| 44 | 2 x M3 x 10 | Item 39, the base cover | 1 |
| 32 | 4 x M3 x 8 socket head | Gripper base onto the tool flange | 15 |
| 25, 28 | 4 + 4 M3 socket head | The jaw linkage, long screws through the links, short into the gears | 16 |
| 31 | 1 x M1.6 x 5 | The SG90 horn | 16 |
Electronics.
| # | الجزء | الكمية | Note | Price |
|---|---|---|---|---|
| 43 | ESP32-WROOM-32 DevKit | 1 | WiFi on board, USB serial for the ROS 2 bridge later | see shop table |
| 41 | PCA9685 driver board | 1 | 16 PWM channels, driven over I2C, a two-wire bus between chips | see shop table |
| 42 | Power switch | 1 | In the base housing face | see shop table |
| 40 | DC barrel jack, 5.5 x 2.1 mm | 1 | Screw terminals, in the base housing face | see shop table |
Not in the assembly model, so these carry no balloon number:
| الجزء | الكمية | Spec | Price |
|---|---|---|---|
| مزود الطاقة | 1 | 6 V; the build runs a HANMATEK HM305P bench supply set to 6.00 V (5 A limit), a fixed 6 V/10 A brick is the set-and-forget option | see shop table |
| Electrolytic capacitor | 1 | 1000–2200 µF across the servo rail | see shop table |
| M3 heat-set inserts | 8 | Brass, for the gripper linkage joints. They replace the nuts Zero used, and they are what ended its slow self-loosening | see shop table |
| أسلاك التوصيل | 1 مجموعة | ESP32 ↔ PCA9685, servo leads | see shop table |
| خيوط PLA | 450 g | 421 g of model plus 28 g of support, see section 5 | see shop table |
| Total | ca. 210–360 €, see the shop table above |
Where to buy it
| Parts used in this project | ca. price (08/2026) | AD · AFFILIATE LINKS |
|---|---|---|
| 6 x joint servo DSSERVO DS3240-180, 36 kg·cm at 5 V, 25T spline | ca. 84–150 € (6 pcs) | Amazon · AliExpress |
| 1 x gripper servo SG90 micro servo, 9 g | ca. 3–6 € | Amazon · AliExpress |
| 1 x 51110 axial thrust bearing 50 x 70 x 14 mm, with washer | ca. 8–12 € | Amazon · AliExpress |
| 2 x 6806ZZ radial ball bearing 30 x 42 x 7 mm | ca. 7–12 € (2 pcs) | Amazon · AliExpress |
| 3 x 695ZZ miniature ball bearing 5 x 13 x 4 mm (the drawing calls it 619/5) | ca. 6–10 € (pack of 10) | Amazon · AliExpress |
| 1 x extension spring 13 N at 30 mm, 40 mm hole to hole. An assortment box is the cheap way in | ca. 9–15 € (assortment) | Amazon · AliExpress |
| 1 x ESP32-WROOM-32 DevKit 38 pin | ca. 7–12 € | Amazon · AliExpress |
| 1 x PCA9685 16 channel I2C PWM servo driver | ca. 5–9 € | Amazon · AliExpress |
| 1 x power supply 6 V, 10 A minimum, 20 A comfortable | ca. 18–30 € | Amazon · AliExpress |
| 1 x DC barrel jack 5.5 x 2.1 mm with screw terminals | ca. 5–8 € (set) | Amazon · AliExpress |
| 1 x rocker switch for the servo rail | ca. 4–8 € (set) | Amazon · AliExpress |
| 1 x electrolytic capacitor 1000 to 2200 µF, 16 V or higher | ca. 4–8 € (set) | Amazon · AliExpress |
| Servo extension leads and jumper wires | ca. 6–10 € | Amazon · AliExpress |
| 3 x M5 hex bolt (2 x 20 mm, 1 x 25 mm) and 3 x M5 nut | ca. 6–10 € (set) | Amazon · AliExpress |
| 14 x M3 socket head screws (4 x 25, 4 x 16, 4 x 8, 2 x 10 mm) | ca. 8–13 € (set) | Amazon · AliExpress |
| 63 x self-tapping screws M2.2 and M2.6, plus 1 x M1.6 x 5 for the SG90 horn | ca. 7–12 € (set) | Amazon · AliExpress |
| 8 x M3 heat-set inserts brass, for the gripper linkage | ca. 7–12 € (set) | Amazon · AliExpress |
| 450 g PLA filament 1.75 mm | ca. 15–22 € (1 kg spool) | Amazon · AliExpress |
| Total | ca. 210–360 € | the spread is AliExpress patience versus Amazon speed |
As an Amazon Associate and AliExpress affiliate, OmArTronics earns from qualifying purchases. The price stays the same for you, and the small commission helps keep these tutorials free.
What that total actually means. The 210 to 360 € is the till price with an empty workshop, and it buys whole packs: ten 695ZZ bearings for the three the arm needs, a spring assortment for one spring, a kilo of PLA for 450 g. Count only the material that ends up inside the finished arm and the value is ca. 160 to 270 €. If you already own a printer, filament, a 6 V supply and a drawer of M3 hardware, the shopping list for this build is ca. 140 to 250 €. All three numbers are dominated by the same row: six DS3240 servos, 40 to 43 % of the total, about 84 € for six with AliExpress shipping times against about 150 € for six delivered from Amazon. That one decision moves the total more than every other line put together.
Two of these rows are worth reading twice before you order, because they are the two places where a near-identical part will cost you the build. The servo must be the DS3240 and not the DS3218و pitch-joint bearing must be the 695ZZ and not the 693ZZ. The note further down this section explains both, and the reasons are arithmetic rather than preference.
The links are search links on purpose, apart from the 51110, which is the exact part on my arm. Individual listings die within months; the search stays alive, and the specs in the left column are what you match against it.
Three parts that look like the right one and are not
Publishing a parts list means somebody spends money on it, so all three of these are worth the space. Each one is a part that sits next to the correct one in a shop listing and differs by a number.
The bearing was the wrong size. Every earlier draft of this list called the pitch-joint bearing a 693ZZ, 3 x 8 x 4 mm, and linked a shop page for it. The assembly says 619/5, which is 5 x 13 x 4 mm and sells as a 695ZZ. What settles it is the fastener list rather than my memory: three M5 bolts and three M5 nuts against three bearings and three sleeves, and an M5 bolt does not fit through a 3 mm bore. The wrong number never reached a published version, which is luck rather than process.
The servo in the CAD is not the servo you buy. The assembly lists the joint servo as an MG996R. This arm cannot run on MG996R. Section 4‘s torque scan puts 23.60 kg·cm on the shoulder at the 250 g maximum, and an MG996R stalls around 11 kg·cm on a 6 V rail, so it could not hold the arm up even empty, never mind loaded. The MG996R is a stand-in body from the CAD library, picked because it has the same standard servo footprint. The part to buy is the DS3240.
And the servo you nearly order is not the servo either. DSSERVO sells the DS3218 and the DS3240 in the same case, with the same 25T spline and the same mounting flange. The DS3218 is a 20 kg·cm servo at 5 V, about 23 at 6 V. The DS3240 is 36 kg·cm at 5 V and 41 at 6. Section 4 put the shoulder requirement at 23.60 kg·cm, so on the standard 6 V rail the two servos land in completely different places:
| At 6 V | Stall | Sustained budget at 63 % | Shoulder needs, at the 250g max | Margin |
|---|---|---|---|---|
| DS3240 | 41 kg·cm | 25.83 kg·cm | 23.60 kg·cm | +2.23 kg·cm |
| DS3218 | 23 kg·cm | 14.49 kg·cm | 23.60 kg·cm | −9.11 kg·cm |
The last cell is the whole story. On a DS3218 the shoulder cannot hold the arm’s own weight inside a duty cycle it survives, because 14.49 kg·cm of budget is already less than the 16.10 the empty arm needs. There is no rail voltage that saves it. If you own DS3218s already, this arm needs different geometry, not a firmware change.
That substitution left one dimension to worry about: the DS3240 is 0.3 mm wider than the stand-in body, and width is the dimension a printed pocket holds tightest. On the built arm the pockets take the DS3240, so the CAD’s clearance absorbs the difference.
One note on that jack: the Inventor parts list says 5.5 x 2.5 mm, while the schematic and the build use the far more common 5.5 x 2.1 mm. Go with 2.1. It is the plug a 6 V lead almost certainly carries, and a 2.1 plug in a 2.5 socket is an intermittent contact on a rail that feeds seven servos.
For the shoulder spring the closest catalogue equivalent I found is the Gutekunst Z-071I, catalogued as 0.8 x 8.8 x 32.3 mm, which is wire, outer diameter and body length inside the hooks. Measured hole to hole that is 39.5 mm, against the 40 mm on my arm. Its published rate is 0.333 N/mm with 1.55 N of initial tension. That is a specification reference rather than a shop link. A cheap assortment box works too, as long as you pick by the 13 N at 30 mm rule in section 5 and not by looks.
The electronics rows should look familiar: ESP32, PCA9685, one supply between 5 and 6 V. That’s Zero’s stack, unchanged on purpose. It was never the limiting factor, and reusing it means everything the Zero series taught about wiring and firmware carries straight over.
Bearing selection: why these three
Three bearing types for seven joints, and the reasoning for each one lives in section 5‘s walkthrough. Two things belong here. ZZ means metal shields on both sides, which keeps print dust out of the races; on a machine that lives next to a 3D printer, that’s not decoration. And the case against plain bushings is arithmetic: a tenth of a millimeter of play across the 60mm shoulder fork is 0.1 degrees of tilt, and 0.1 degrees at 485mm is 0.8mm at the fingertips.

Servo selection: the DS3240, and what “40 kg” means
The joint servo is the DSSERVO DS3240-180, and the first thing to sort out is the big number printed on its side.
The case says 40 kg. The datasheet’s own spec table has only two voltage columns: 36 kg·cm at 5 V و 45 kg·cm at 6.8 V. So where does 40 come from? Interpolate between those two points and you land at 41 kg·cm at 6 V, and the datasheet’s product description is, word for word, “6V 40kg RC Digital Servo”. The 40 is the 6 V figure, rounded down. It’s real, it’s just quoted at a voltage the spec table never lists, and it is emphatically not what you get at 5 V.
That distinction runs through this whole build, so here is the full interpolation:
| Rail voltage | Stall torque | No-load speed | Stall current | Shoulder at 23.60 kg·cm |
|---|---|---|---|---|
| 5.0 V | 36 kg·cm | 0.20 s/60° = 300 °/s | 3.10 A | 65.6 % of stall, over the line |
| 6.0 V, standard | 41 kg·cm | 0.183 s/60° = 327 °/s | 3.54 A | 57.6 % of stall |
| 6.8 V | 45 kg·cm | 0.17 s/60° = 353 °/s | 3.90 A | 52.4 % of stall |
(5 V and 6.8 V are datasheet rows; the 6 V line is linear interpolation between them.)
Every number in this post is the 5 V case, because that is the pessimistic floor and because a sagging supply lands you there anyway. Run the rail at 6 V and you get the numbers above for free, which is where the 6 V figures printed under the payload table in section 4 come from. You cannot go higher. The SG90 gripper shares the rail and 6 V is its ceiling. The nice coincidence of this design is that the gripper’s limit sits exactly where the joint servos deliver their advertised 40 kg.
The naming fog caught me too. Between near-identical DS3218 and DS3240 listings I spent a while convinced I owned the other model. Read the datasheet PDF, not the case.
The numbers that earned it the job, all from the DS3240 series datasheet:
Stall torque 36 kg·cm at 5V, 41 at 6V. Section 4 showed the worst joint needs 23.60 kg·cm at the 250g maximum, which asks for a servo rated at least 37.5 kg·cm. That is above what the DS3240 gives at 5 V and inside what it gives at 6 V, which is why the arm runs on 6 V. One class down, the 18 to 24 kg·cm bracket, would put the shoulder past 100 percent at useful payloads. One class up costs more, weighs more, and buys torque the printed structure can’t use. That leaves the 36 kg·cm bracket, and the DS3240 sits in it.
Dead band 3 µs. Over the 500 to 2500 microsecond range mapping to 180 degrees, that’s 0.27 degrees, and it’s the repeatability floor of the whole arm. For scale, 0.27 degrees at the base is about 2.3mm at the fingertip at full reach. That is the number to quote when someone asks how precise the arm is.
Speed 0.20 s/60° at 5V, which is 300 degrees per second unloaded. The firmware never runs the joints that fast, and each software layer above it picks its margin against this documented ceiling rather than against a guess.
Stall current 3.1A at 5V (3.9A at 6.8V). The entire power budget in section 4 comes from this figure.
The mechanical rest: 342:1 metal gear train, double-bearing output shaft, IP66 sealed case, 60g, 25T output spline, and an accepted update rate of 50 to 330 Hz. The update rate turns into a wiring decision further down this section.
And the case for using the same servo six times, including at joints that will never see a tenth of its torque, comes from section 4. Spares, calibration and mounting geometry all collapse to one of each, and six of one part is cheaper than three of two.
The gripper servo, and why it rewired the electronics
The gripper runs an SG90: 9 grams, analog, the same micro servo Zero used. Nothing to gain from more, since the gear reduction makes the jaw force and the servo only has to move.
But the SG90 forced a real topology decision. It’s analog and only accepts the classic 50 Hz update rate, while the DS3240s take up to 330 Hz. The joint servos benefit from the faster bus, so this build drives the PCA9685 at 250 Hz, five times Zero’s rate, which buys finer pulse resolution and quicker response on every joint. One board means one frequency, and the SG90 can’t sit on that bus. So the gripper signal comes from an ESP32 pin with its own 50 Hz timer instead. The smallest servo on the arm reshaped the wiring: six joints on the PCA9685 at 250 Hz, one gripper on its own pin at 50 Hz. Full detail in section 8.
ESP32 and PCA9685
Both carried over from Zero for the same reasons, so I’ll keep it short and point you at the Zero post for the long version. The ESP32 brings WiFi, enough compute for a web server alongside a motion loop, and a USB serial port for the ROS 2 bridge later. The PCA9685 exists because PWM generated in dedicated hardware doesn’t jitter when WiFi interrupts fire. You set a pulse width over I2C and it holds it. Six servos of this class on hardware PWM cost two wires.
Both boards live inside the base housing, so there is no breadboard hanging off the back of this arm.
Power supply sizing
Section 4 did the math: 4.3A typical, 19.3A only in the crash case. A 5V 10A supply covers everything you’d call operation, and the 1000 to 2200 µF capacitor absorbs the transients.
The rail voltage follows one line of reasoning, the SG90 ceiling from above: the shared rail stays at 6V even though the DS3240s would take 6.8V and reward it with 45 kg·cm. That costs the joint servos their top 4 kg·cm and buys a single shared rail with no second supply and no level shifting, which on a machine with seven servos is a trade worth making.
The supply on my bench is a HANMATEK HM305P, a small programmable unit set to 6.00 V, and its 5 A current limit is below the 10 A this section recommends. It works anyway, for two reasons: the motion core’s speed caps keep the arm’s typical draw around 4.3 A, and when a move does ask for more, the supply folds into constant-current mode and the rail sags instead of anything burning. That sag lands you at the 5 V torque column, which the whole post treats as the floor. For a permanent installation a fixed 6 V/10 A brick is the better choice; the bench supply’s real advantage is the display, which is also how 8 V ended up on the rail exactly once, as section 4 confessed.

7. Assembly guide
The build has a natural order, and it isn’t the one you’d guess. The sequence below is the one the CAD assembly plays back: you don’t start with the arm, you start with the box the arm stands on, because the electronics have to go in before the base is closed.

Before you start
A stalled joint servo presses with its full 41 kg·cm on the 6V rail. Across the whole 485 mm to the fingertips that is a modest 0.85 kg of force. Three centimetres from a joint axis, which is about where a finger gets caught, the same stall is roughly 14 kg of squeeze. Nothing here is lethal and none of it needs protective equipment, but the arm can hurt you at close range, so five habits are worth having before the first screw goes in.
- Keep your fingers out of the joint gaps while the servos are powered. The forks close on themselves, and a servo does not notice an obstruction.
- Switch the servo rail off while you work on the arm. The switch in the servo + line does that, and the ESP32 keeps running on USB, so the web page stays up while nothing can drive. Switch it back on with the arm where the firmware last left it, or with your hands clear.
- Clear the table before you first enable the servos. By design the arm does not move on boot, so the movement to plan for is the first one after output is enabled, not the boot itself.
- Hands clear before you confirm a pose. After a power cycle or an upload the firmware asks where the arm is. Whatever you answer, it switches the servos on at those angles. Section 8 has the mechanism.
- Respect the current, not the voltage. Six volts will not hurt you. Ten to twenty amps into a shorted servo lead is a burn and a smoke hazard, so check polarity before the switch goes on and keep bare ends apart.
And one rule that will save you a part rather than a finger:
Centre every servo electrically before you put a horn on it. Wire the servo, power the board, command 90 degrees, and only then push the horn on at the angle the CAD wants. A servo mounted at the wrong horn angle loses part of its travel at one end and hits its mechanical stop at the other. It’s the single most common way to ruin an evening on a build like this.
Every one of the sixteen steps below has a CAD animation of the operation. They’re short, they loop, and they show the part going in from the angle you’ll actually be looking from. The order they play in is the order to build in, and I’ve matched the text to it rather than the other way round.
Step 1: Electronics into the base housing

Wire everything before you close anything up. I put the wiring first because the base cannot be opened again afterwards: once the rotating platform is on, you cannot reach in without taking the arm apart. Section 8 has the wiring in detail; do that first and come back here.

Step 2: The base servo

Screw it down, but leave the horn off. The horn goes on in step 4, after the servo has been centred electrically, which is the rule at the top of this section.

Step 3: The two base bearings

Check both seats with a caliper before you press anything. FDM bores print undersize more often than not, and this is the step where I expect most builders to lose a part. A 42mm seat that came out at 41.7 will not take a 6806ZZ, and forcing it splits the print along a layer line, usually while you’re leaning on it with a vise, which is exactly when you can’t stop.


Step 4: Platform onto base

From here on the platform turns on steel. Grab the assembled base and try to rock it: a 70mm circle of bearing resists you, not a splined shaft in a plastic pocket.


Step 5: The upper arm, on the bench

This one link carries two servos, one at each end: J2 drives the shoulder below it and J3 drives the elbow above it. Both horns point outward, so both joints close later against parts that are already on the machine.
This step is the one I had in the wrong place for a long time, and the animation is what corrected it. Build the upper arm complete first. Once it is hanging in the shoulder fork you are working one-handed, over a base that wants to tip, with a screwdriver you cannot get square to the screw.

Step 6: The shoulder, and the spring

The platform carries a two-armed fork with a plain bore in each arm and no servo pocket in either: the J2 servo went into the upper arm back in step 5. Press the 695ZZ into one fork arm, drop the finished upper arm between the arms, and screw the horn to the opposite fork arm. Then slide the sleeve through the bearing and run the M5 x 25 in with its nut on the outside; the shoulder gets the long bolt of the three.
Tighten until the arm swings freely under its own weight with no side play. That combination, free but not loose, is how a fixed-loose arrangement is supposed to feel. If the joint stiffens as you tighten, your sleeve is too short and the fork arms are being squeezed inward.


The spring closes this step. Hook it between the two printed lugs, one on the platform and one on the upper arm, and pin both ends. It pulls the arm up, which is its whole job here.

Step 7: The roll-joint pattern

This is the second of the two patterns, and from here on the arm is these two repeating. A roll joint has no fork and no loose side. The servo sits in the segment, the horn goes on its spline, and the next segment screws straight onto the horn.


Step 8: The elbow closes

Same rule as the shoulder. Tighten until the joint is free but has no side play, and stop when it stiffens.
Step 9: The forearm roll, bearing and horn

This is J4, and it is the only joint outside the base that gets a 6806ZZ; section 5 explains why.
Step 10: The forearm roll segment onto the arm

Slide the segment over the horn, run the four screws in, and check that the roll turns freely before you move on. The M5 goes in from the side at the end of this step; its nut waits for the next one. This is also the last easy moment to route the wrist cables through the segment, so do that now rather than after step 12.
Step 11: The wrist servo

That nut is the third and last of the three M5 sets, and it closes the bolt you drove in at the end of step 10. Centre the servo before the horn goes on. Every joint from here out is close enough to its mechanical limits that a horn one spline tooth off will cost you visible travel.
Step 12: The wrist pitch joint

This is the last of the six DS3240 servos, and here the fork carries the servo body itself. Four screws hold it, the same as everywhere else. Centre it before the horn goes on, and check the joint swings freely before the next step closes access to these screws.
Step 13: Closing the wrist

Once this screw is in, the wrist is a closed unit. Run all three wrist joints by hand through their travel once; anything that rubs now is two steps of disassembly later.
Step 14: The tool flange

J6 is the exception here. It runs on the servo horn alone, with no bearing. Section 5 justifies it with the number, 0.01 kg·cm of worst-case moment, which is about a thousandth of what the shoulder carries.
Step 15: The gripper base

Four M3 socket heads through the base into the flange. Snug is enough; the printed threads strip before the screws do.
Step 16: The gripper

Close the jaws by hand before you mount the servo horn, so you know which way the gear pair travels, then centre the servo and set the horn at the fully-open position.
After the sixteen steps: cable routing
Seven servo cables have to reach the base without getting pinched at a joint or pulled tight at full extension. Run them along the inside of the links, leave a service loop at every joint, and check the whole range of every joint by hand before power goes anywhere near the arm.

Every fastener in these sixteen steps is itemized in section 6, including a which-screw-goes-where table keyed to the drawing’s balloon numbers. Two things there are not, because the assembly model does not carry them: the M3 heat-set inserts in the gripper linkage, and the two small screws that come in the SG90’s own bag.
8. Wiring and first power-up
Two circuits, and the wiring is mostly about keeping them apart: a signal path from the ESP32 to the servos, and a power path from the supply to the servos that never touches the ESP32’s regulator.

The connections

| من | To | Note |
|---|---|---|
| ESP32 GPIO 21 (SDA) | PCA9685 SDA | I2C at 400 kHz |
| ESP32 GPIO 22 (SCL) | PCA9685 SCL | |
| ESP32 3V3 | PCA9685 VCC | logic supply for the driver chip only |
| ESP32 GND | PCA9685 GND | common ground, non-negotiable |
| ESP32 GPIO 13 | SG90 signal | own 50 Hz timer, not on the PCA9685 |
| Supply + (6 V) | PCA9685 V+ screw terminal | plus 1000–2200 µF across it |
| Supply − | PCA9685 GND screw terminal | |
| SG90 + / − | servo rail | the V+/GND pins of PCA9685 channel header 12 |
| التبديل | in the servo + line | use it while flashing and testing |
| USB | ESP32 | this is the only thing powering the ESP32 |
The arm runs on two supplies, and they are easy to mix up. The 6 V brick powers the servos and nothing else. The ESP32 is powered over USB, and it in turn feeds the PCA9685’s logic pin from its 3V3 rail. Unplug the USB and the whole thing is dead even with the 6 V connected. Switch off the 6 V and the ESP32 still boots and still serves the web page, but nothing moves. That second state is useful, and the first power-up below runs in it on purpose.
The ESP32’s VIN or 5V pin is not a power source for servos. It’s a regulator rated for a fraction of one servo’s stall current, and it sits directly on your USB port.
Channel assignment
The firmware expects J1 to J6 on PCA9685 channels 0, 3, 4, 7, 8 and 11. That spacing isn’t arbitrary: I spread the servos across the header to leave room for the connectors and keep the wiring readable.
| مشترك | J1 | J2 | J3 | J4 | J5 | J6 | J7 |
|---|---|---|---|---|---|---|---|
| Channel | 0 | 3 | 4 | 7 | 8 | 11 | ESP32 GPIO 13 |
The same six numbers appear in the sketch as SERVO_CHANNELS[] = {0, 3, 4, 7, 8, 11}, and they match the drawing above net for net. The firmware also prints the mapping in its boot log, so you can read back what it thinks is connected. If a slider moves the wrong joint, change that line and leave the wiring alone. The sketch says as much in its own header comment.
Checked on the built arm: the harness follows the schematic, and the boot log’s channel map matches what the sliders move.
The 250 Hz decision, and the trap inside it
The PCA9685 drives the six joint servos at 250 Hz, well inside the DS3240’s 50 to 330 Hz window. Five times the classic rate takes one count of the PWM register from 0.44 degrees at the joint down to 0.086. Since the servo’s own dead band is 0.27 degrees, running at 250 Hz moves the resolution limit off the electronics and onto the servo, which is where it belongs.
Now the trap. The PCA9685 doesn’t run at the frequency you ask for. It divides its internal oscillator by an integer prescaler:
f = osc / (4096 × (prescale + 1))
At 50 Hz the rounding error is 0.06 percent and nobody notices. At 250 Hz the nearest prescaler is 23, which gives 254.3 Hz, 1.7 percent off. The chip therefore runs a shorter period than you asked for, so pulse counts computed from the nominal 250 Hz come out 1.7 percent short. That is not a constant offset but a gain error: 0.8 degrees at one end of the travel, 2.3 in the middle and 3.8 at the other end, on every axis. The 0.086 degrees per count above comes from that same 254.3 Hz: an exact 250 Hz would give 0.088, and the extra 4.3 Hz buys the last two thousandths. So I compute every count from the prescaler that is actually in the chip, never from the frequency I asked for. If you write your own control code, copy that habit.
First power-up
Install the two libraries the sketch needs (Adafruit PWM Servo Driver Library, ArduinoJson), flash it, and leave the servo power switch off for the first boot. Watch the serial log: it prints the channel mapping, the real PCA9685 frequency, and whether the gripper timer attached.
Then join the WiFi access point OmArmOne (password omarmcontrol) and open http://10.10.10.1. The web app is carried inside the sketch and writes itself to the ESP32’s filesystem on first boot, so there’s nothing else to upload.

The part that matters on a machine this heavy is what happens after a reset. The arm has no position feedback, so the firmware wakes up with no idea where it is. The naive behaviour, and what my own earlier version of this code did, is to drive every joint to zero at full speed on boot. That is a crush hazard that fires on every single upload. Instead:
- Two records hold the pose. A copy in RTC memory is refreshed on every servo tick, which keeps a reset in the middle of a move safe, and a flash copy is written a moment after the arm comes to rest. RTC memory survives a CPU reset and not a power cycle, which is the same line the reset-reason logic draws.
- After a software reset or a crash the firmware restores that pose and re-commands the servos exactly where they already stand, so nothing moves.
- After a power-on, a brownout, or an upload, it drives nothing at all. The web app raises a warning and waits for you to declare the pose. An upload lands here because it resets the board through the EN pin, which the chip reports as a power-on. Fold the arm in or support it before you flash.
- Declaring the pose is a jump, not a move. Both confirm buttons, and a SETPOS over the wire, switch the servos on at the angles you declare, at once. If the arm is not really there it snaps to them at full speed. The four-second rule below applies to the first commanded move, not to this. Hands clear.
- The first move after any boot is forced to take at least four seconds, no matter where the speed slider sits.
- The emergency stop freezes the arm with the servos still holding, and it latches; section 9 has the release flow.

Two quirks on the servo side of a reset. The gripper hangs on GPIO 13, so it stops receiving pulses and goes limp. Empty the jaws before you flash. And J1 to J6 do keep their PCA9685 pulse through the reset itself, but the sketch then parks every channel full off before it touches the driver, deliberately: the chip’s registers survive a reset holding counts computed for the previous frame rate, and re-running them would snap a joint toward its stop. No pulse is the safe intermediate state. The six channels come back only once the firmware accepts a pose, so in the confirm-first case they stay unsignalled, and therefore limp, until you answer. The rail is still live; it is the pulse that is missing.
None of this replaces the switch in the servo power line. Use it.
That is the firmware side of the first power-up. The next section is the same moment from the browser.
9. The web interface
Everything up to here has been metal, plastic and wire. Now you get to drive it.
You already did the two steps at the end of the last section: join OmArmOne, then open http://10.10.10.1. Type the http:// or the phone will treat it as a search term. There is no app, no account, no router and no internet involved, which lets the arm work in a workshop with no signal or a classroom where nobody is allowed to install anything.
One warning first, because it will otherwise cost you ten minutes. Phones treat a network with no internet behind it as suspect. Android asks whether you want to stay connected and can start routing traffic over mobile data instead, and iOS marks the network as having no connection. If the page stops answering for no visible reason, that is usually the cause. Turn mobile data off while you drive the arm, or answer the prompt when it appears.
What you are looking at


Seven sliders run bottom to top in the order you meet the joints on the machine: J1 base, J2 shoulder, J3 elbow, J4 forearm roll, J5 wrist pitch, J6 tool roll, and J7 the gripper at the top.
The number in each label is the angle the firmware was told to reach. Nothing on this arm measures an angle, so nothing on this page can show you one. Whether the servo arrived is a separate question, and section 10‘s repeatability check is how you answer it on your own machine.
In a window wider than 1024 pixels each slider is tied to a picture of the arm by a leader line. The lines are redrawn when you scroll the slider column or resize the window, and once a second regardless, so a late-loading image cannot leave them pointing at the wrong row. Anything narrower, a phone or a desktop window snapped to half the screen, drops the picture and keeps the labels.
Three controls sit above the sliders. A connection dot goes green only when a reply actually arrives, though it is not a heartbeat: a request that hangs rather than fails leaves it green until the browser gives up. HOME runs a coordinated move to J1 0, J2 45, J3 45, J4 0, J5 0, J6 0, J7 61, which is where the arm should be parked. The emergency stop sits in the page header, and the header is the one part of the layout that never scrolls, so it stays under your thumb wherever you are on the page.
One speed slider governs the whole arm. It runs 1 to 100 percent, which the page turns into a velocity ceiling of 15 to 180 degrees per second and an acceleration ceiling four times that. Those two numbers go to the firmware, which enforces them inside its own control loop, so a slow setting stays slow even if the page freezes mid-drag.
You drive joints here, not points in space. I left inverse kinematics out of this interface entirely. Cartesian control arrives in Part 3, where a planner owns it and can check a path for collisions before anything moves, and a half-working version of that in a browser would have been worse than none.
What one slider drag actually does

Each slider covers 0 to 180 degrees in whole steps, so it fires an event on every degree your finger crosses. Sending all of them would bury a microcontroller that is also feeding the servo driver 250 times a second. The page therefore sends the first value at once, then at most one every 50 milliseconds, and values in between overwrite each other rather than queueing.
The awkward case is the end of the drag. Lift your finger inside one of those 50 millisecond windows and the last value never goes, which leaves the arm a few degrees short of where you put it. So the release sends it a second time, from change, pointerup و touchend, because no single one of those three fires reliably on every browser. The label updates on every degree regardless, so the number tracks your finger even while the requests lag behind it.
In the other direction the page asks for STATUS every two seconds and gets the whole machine state back as one JSON object: the seven angles, the pose that was in flash at boot, whether a planned move is running, whether the position is known and confirmed, whether the emergency stop is latched, the I²C error count, and how many servo ticks arrived late. The page displays only a few of them. The rest are there for a terminal or a script, and for the evening you spend working out why one joint stutters.
The sliders follow that answer only while the firmware is running a planned move of its own, such as HOME or a sequence, and only once the slider has been still for 400 milliseconds. Without that guard a two second old status would push the slider backwards under your finger while the arm was still catching up to you.
Every command is one line of text. It travels to /api/command inside a one field JSON body, {"cmd":"HOME"}, and the answer comes back as JSON. MOVE:0,45,45,0,0,0,61 is a coordinated move in which all seven joints share one duration and arrive together. 4,120 sends joint 4 to 120 degrees on its own. That same one line grammar is what the ROS 2 firmware in Part 2 speaks over the USB port, into the same motion code.
The screen I hope you only see once

There are two ways this arm refuses to move, and it is worth knowing both before you meet either. A red banner across the top of the page means the firmware does not know where the arm is and wants you to tell it. No banner and nothing happening means the emergency stop is latched, and the fix is the Release the stop button. In both cases the sliders still slide and the numbers still change, because nothing greys them out. The refusal happens in the firmware, and the reason appears in the status line for a few seconds.
The why of the first refusal, reset reasons, RTC memory, the flash pose, is all section 8; this is what it looks like from the browser. Printed in the banner is the pose that was in flash when the firmware came up. You hold that list of angles against the arm in front of you and decide which of the two buttons underneath is telling the truth.
Arm is still there, enable servos claims the arm is standing where the firmware left it. The arm is at 0°, enable servos claims you have wound every joint by hand to its 0 degree end, gripper included. That second pose is the one you can hit accurately by hand, because every joint runs into a hard stop there. It is not the HOME pose, which parks the shoulder and elbow at 45 degrees.
Both buttons switch the servos on at the angles they name, so a wrong claim makes the arm jump to them at full speed. It is why each button arms on the first tap and only fires on the second, why the warning above them says to keep your hands clear, and why they are labelled with the claim you are making rather than with what the button does.
The emergency stop works the other way round. Pressing STOP freezes the arm with the servos still powered and still holding, because cutting drive to an arm this heavy would drop it. Then it latches, and the firmware refuses every motion command, including ones already on their way, until you press Release the stop. That matters more than it looks. A value can already be sitting behind the 50 millisecond throttle when you hit the button, and a second phone on the same network knows nothing about your stop at all.
If the arm is doing something dangerous and the page is not answering, the switch in the servo power line is the last resort. It takes the holding torque with it, so the arm falls. Clear whatever is underneath before you reach for it, and reach for the on-screen stop first.
Recording something to show

Set the arm to a pose, press + Add Current Pose, repeat. Press Play Sequence and it walks the list with a delay you pick between 0.1 and 3 seconds, reaches the end, and starts again from the top until you press Stop. That Stop is the same emergency stop as the one in the header, latch and release included.
This is the smallest possible version of teaching by demonstration, and it is enough to put the arm in front of people long before the ROS 2 machinery exists.
The list lives in the open page and nowhere else. Reload the tab and it is gone. Export writes it to omarm_one_sequence.json, which is the only copy that survives, and import reads it back. A six-value sequence from OmArm Zero imports as well: J1 to J5 map straight across, Zero’s gripper becomes J7, and the new tool roll starts at 0. None of it is stored on the ESP32. The firmware has no sequence command at all, and only knows how to go to a pose.
How the page gets onto your phone
Skip this if you only want to drive the arm. It matters if you want to change the interface.

The interface is seven ordinary files in data/, next to the Arduino sketch: one HTML page, one script, one stylesheet, a manifest, a service worker and two images. Any editor will do.
build_web_assets.py turns them into web_assets.h. The five text files get gzipped, the two PNGs get downscaled and reduced to a 255 colour palette, and each one becomes a C byte array the compiler packs into the firmware. The script prints the total when it runs: 31,375 bytes, which it rounds to 30.6 KB. On first boot the sketch unpacks them onto the ESP32’s filesystem and serves them from there.
Compiled, the whole sketch lands at 1,058,099 bytes, 80 percent of the ESP32’s 1.25 MB app partition, and the packed interface accounts for a modest 31 KB of it: the five gzipped text files come to 12.4 KB and the two quantized PNGs to 18.2 KB. The web app costs less flash than the WiFi stack it rides on.
The version is the part I would keep in any project like this. web_assets.h carries a hash of everything in data/, and the sketch remembers the hash it last unpacked. Change one character of the interface and the hash changes, so the next boot rewrites the files. Change nothing and it leaves the flash alone.
Web app up to date (version 0x287757BE).
That line in the boot log means the page in your browser matches the firmware behind it. It also removes the separate filesystem upload step, and with it the question of whether you remembered to do it.
What it is not
The access point has a fixed password compiled into the sketch, and /api/command has no authentication of any kind. Anyone in radio range who joins the network can command the arm, and cross-origin requests are allowed, so a page from somewhere else that they already have open can post to the arm as well. On a desk that is fine. In a room full of people it is not, so change the password before you take this arm anywhere public, and keep the servo power switch within reach of whoever is standing closest.
Outbound there is nothing at all. The interface loads no CDN, no font host and no analytics endpoint, which is why it works with the arm sitting on a bench in a basement. There would be no internet on that network to reach anyway.
10. Testing and validation
Five checks, in the order I would run them. The first is the one this post’s headline claim lives or dies by.
The payload test
I computed the payload envelope before the build so that this test would have a prediction to fail against. Section 4 predicts the following.
| Test | Radius | Predicted sustained, 6 V | At stall | 5 V for comparison |
|---|---|---|---|---|
| Nominal check | 300 mm | 325 g | 830 g | 219 g |
| Maximum check | 300 mm | the 250 g maximum uses 57.6 % of stall | n/a | would need 65.6 %, over the line |
| Maximum edge | 370 mm | 252 g | 661 g | 167 g |
| Full stretch | 485 mm | 175 g | 488 g | 110 g |
Procedure. Mark the working radius on the table, measured horizontally from the base axis. Weigh the test object on a kitchen scale, note the actual mass, and don’t round in your favour. Command the arm to a pose that puts the grasp point on the mark with the arm well stretched, close the gripper, and lift. Then leave it there for sixty seconds and touch the shoulder servo’s case. Warm is expected. If you cannot keep a finger on the case, the joint is over the sustained line whatever the arm is holding. An infrared thermometer turns that into a number worth writing down.

What counts as a pass: the arm lifts the mass, holds the pose without sagging, and the shoulder servo case is still touchable after a minute. Sag under load is the interesting failure, because it means either the servo is past its torque budget or a bearing seat is loose. Those two feel different. Torque sag is smooth and continuous; a loose bearing gives you a step, a click, or a pose that drifts when you nudge the arm sideways.
Second run at 6 V. Same masses, same radii. The whole table shifts up by about half for one volt on the rail, which makes it the cheapest upgrade available on this machine. Section 6 explains why 6 V is the ceiling.
Four more checks before you call it finished
Range per joint. Drive each joint slowly across its range with the arm supported, and approach both ends carefully. Zero is not free space: at 0 degrees the arm folds against its own mechanical stops, and the firmware will happily command that, because its only limits are the servo’s full 0 to 180. So the pass criterion is that each joint travels smoothly up to its first contact, not that it sweeps a clear 0 to 180. Stop at the first contact, back off, and note the angle. Compare it against the CAD: a joint that stops well short of its drawn limit usually has its horn on one spline tooth off.
Repeatability. Mount a pen in the gripper, tape paper to the table under it, and teach one pose with the pen tip just touching. Then alternate: drive to HOME, back to the pose, take the dot; drive to a second pose on the far side of the workspace, back, dot. Ten dots per direction, measured in the plane of the paper only. The servo’s dead band puts a floor of 0.27 degrees under the result, which is a Ø 2.3 mm circle at full reach, so a cluster that size is the arm performing at its physical limit. A cluster much wider than that is mechanics: a loose sleeve, a bearing not fully seated, or a horn screw backing out, and two separated clusters, one per approach direction, are backlash.

Base stiffness. The bearing upgrade earns its place here, and the direction you push decides whether the test means anything. Clamp or weight the base to the bench first, extend the arm horizontally, then press down on the wrist with about 5 N, roughly a full 500g can, and watch the fingertip. Down loads the 51110 and the 6806ZZ, because a vertical force at reach is a tipping moment about the base. Release, and the deflection should be elastic and return to zero. A rocking motion with a dead zone in the middle means a bearing is not fully seated or the platform is loose on the thrust washer.
Pushing sideways tests something else entirely. That direction runs through J1’s gear train, where a little backlash is normal and says nothing about the bearings.
Weight. Done: 1283 g complete on the kitchen scale. The component build-up in section 5 predicted about 1.1 kg for the same scope, so the estimate ran some 190 g light; wiring, horns, threaded inserts and 86 screws sat in that table as a single estimated 90 g row, and they weigh nearly three times that. The number is entered as MEASURED in the analysis script, and because it lands within 0.1 percent of the solid model’s total, every torque and payload figure in this post stands unchanged.

The measured runs, payload lifts and temperature checks are in the build video; the protocols above are exactly what they follow.
11. Troubleshooting
These symptoms come from the predecessor’s build and from bringing this firmware up on the bench.
Every joint is off by up to 11 degrees, consistently. Your PCA9685 oscillator constant is wrong. Half the example sketches on the internet declare 27 MHz; the chip’s datasheet says 25 MHz. Feed 27 MHz to a chip running at 25 and every pulse stretches by 8 percent, which lands 1500 microseconds at 1620 and puts the joint 11 degrees off. Use 25 MHz unless you have measured your own board.
All joints slightly off and the zero point shifted. Prescaler rounding, described in section 8. Compute pulse counts from the prescaler actually in the chip (254.3 Hz at a 250 Hz request), not from the frequency you asked for.
The wrong joint moves when you drag a slider. Channel mapping, not wiring. Fix the SERVO_CHANNELS line, reflash.
Joints jitter or hum while holding still. Two causes worth separating. If the twitch is on all joints, it’s PWM jitter. Generate the signal on the PCA9685’s own hardware instead of on interrupt-driven GPIO, which is exactly why the driver board is in the BOM. If a single joint hums, it’s usually mechanical: a sleeve tightened until the fork binds, or a horn screw catching.
The whole arm goes limp at the end of a flash, and stays limp until you confirm the pose. Expected: an upload counts as a power loss. Section 8 has the full sequence, and why you fold the arm in and empty the gripper first.
Nothing moves at all on a brand-new board. Also expected, and deliberate: I would rather the arm refuse than guess. Declare a pose with SETPOS from the web app; section 9 shows what that looks like.
The arm moves sluggishly and the ESP32 reboots under load. Supply sag: undersized power supply, missing bulk capacitor, or servo power taken from the ESP32. A 10 A supply and 1000 to 2200 µF across the rail fix the first two; the third one you fix by rewiring, and the table in section 8 shows how.
The ESP32 gets hot, the USB port on your PC disappears, and the arm twitches on its own. Overvoltage on the servo rail. The DS3240 tolerates 6.8 V, the SG90 6 V, the PCA9685’s V+ 6 V, so eight volts is outside all three, and the PC shutting down its USB port is a protection circuit doing its job. Section 4 has the incident. Check the rail with a meter before every session, and put a switch in the servo line so the first power-up after a wiring change is deliberate.
In my case the 8 V came from a bench supply still set for another project. Everything survived the excursion; treat that as luck, not as a rating.
A joint stalls near one end of travel. Servo horn one spline tooth off, or a printed part interfering. Recentre the servo electrically, remount the horn.
A bearing won’t go into its seat. Measure the bore. FDM bores print undersize; the fix is a dimensional test print and a scaling tweak in the slicer, not a hammer. Step 3 in section 7 is where that caliper check belongs.
12. FAQ
Can I use cheaper servos, like the DS3218 or an MG996R? Do the arithmetic first. The shoulder needs 23.60 kg·cm at the 250 g maximum, and the sustained rule allows 63 percent of stall. That puts the minimum stall torque at 37.5 kg·cm. An MG996R at 11 kg·cm isn’t in the conversation. A DS3218 doesn’t make it either: 63 percent of its 23 kg·cm at 6 V is 14.49 against the 23.60 the shoulder needs, and it misses even the empty arm’s 16.10. And on AliExpress the DS3218 is barely cheaper than the DS3240 anyway, roughly 82 € against 84 € for six, so the swap saves about two euro and costs you the shoulder joint.
Should I run 5 V or 6 V? 6 V, if your gripper servo is fresh and you trust your supply’s regulation; section 4‘s payload table says what the volt is worth. 6 V is the ceiling, not a suggestion: the SG90 shares the rail and dies above it.
Do I need the spring? No. Every number in this post was computed without it. It’s a safety factor: it takes load off the shoulder, keeps the servo cooler on long holds, and gives the arm a gentler failure mode if the servo ever loses power while extended. Build the arm without it and the specs still hold.
Can I skip the bearings and mount everything on servo horns? You can, and it will work for a while. What you lose is the thing that makes this arm different: the moment ends up in the servo’s gear train, and the wear shows up as play at the gripper, scaled by however far the gripper sits from that joint. The 0.8mm that section 6 works out at the fingertip is not an angle error you can calibrate away, because it changes with load and with direction.
Why does the base only rotate 180 degrees? Because the workspace is a forward half-shell by design. A full circle would need slip rings or a cable management scheme for seven servo cables, and Parts 4 and 5 of this series want one clean scene in front of the arm anyway.
Do I need ROS 2 to use this arm? Not for this part. The ESP32 serves its own web app over its own access point, so a phone or laptop is enough. ROS 2 arrives in Part 2 with the digital twin, over the same USB port and the same firmware conventions.
The wrist axes are tilted 10 degrees. Is that a problem? Only if you want closed-form inverse kinematics. The wrist isn’t spherical, so poses have to be solved numerically. MoveIt 2, which takes over in Part 3, does that anyway.
Can it lift 500 grams? Not sustainably. At a 300mm radius the stall ceiling is 830 grams on the 6 V rail, and stall is where servos cook themselves. 500 grams for a photo, briefly, yes. As a working payload, no. That’s a different machine.
My printer isn’t a Bambu P1S. Can I still print this 3D printed robot arm? Any printer with a bed around 256mm and decent dimensional accuracy will do. The one thing to check before printing the base is bore accuracy, since the bearing seats are the only truly critical dimensions in the whole model.
13. What comes next
The arm is drawn to hold its 250 grams, and every joint that carries a moment carries it through steel instead of a servo’s gear train. That is the one decision that separates this 3D printed robot arm from the ones that develop a wobble after a month. But every move it makes, I still command by hand.
Part 2 fixes that. The CAD model gets exported into a URDF, a machine-readable twin of the geometry in section 4 down to its half-millimeter offsets, and comes alive in RViz and Gazebo. By the end of Part 2 the twin mirrors the real arm and a game controller drives it. From there: MoveIt 2 planning on real hardware in Part 3, camera-guided pick and place in Part 4, and in Part 5 the arm grasps named objects with no markers at all.
If you build one, I’d like to see it. And if you find something wrong in the numbers above, tell me. The analysis script that produced them is in the repository, seeded and reproducible, so an error is findable rather than arguable.
Resources
- STL files and build package (free right now): OmArm One complete package in the shop — all 23 STLs, print plates, STEP + Fusion 360 source, drawings with BOM, wiring, firmware, tests and the 99-page PDF build guide
- Firmware, analysis script and CAD: the repository goes public with this post; the link lands here
- Spring measurement and substitute selection:
01_repo/kinematics/spring-rate-measurement.md - Full engineering study, every number derived:
01_repo/docs/OmArm-One_Engineering-Study_From-Robot-to-Requirements.md - OmArm Zero, the predecessor series: Part 1
