Build a mini ESP32 robot arm you control from your phone

Build a small robot arm with four servos and 3D-printed parts, then drive it from your phone. The ESP32 makes its own Wi-Fi network and serves a control page with sliders for the base, shoulder, elbow and gripper. Save poses...
  • Level:Advanced
  • Duration: 4 h
  • Ages 12+
  • Platform: ESP32

Tools for this lesson

Helpful for this build. Open a tool to see tips and more projects that use it.

Build a mini ESP32 robot arm you control from your phone – EEMENTOR STEM™ lesson

Factory robot arms weld cars, pack chocolates and sort parcels, all day long and without getting tired. Most of them are not clever at all: a person shows the arm a few positions once, and the arm repeats them thousands of times. In this capstone you build a mini version of such an arm. Four servos turn the base, the shoulder, the elbow and the gripper, and the parts come out of a 3D printer. The ESP32 makes its own Wi-Fi network, so you don't need a router: join it with your phone, open the control page, and drag the sliders to move each joint. Then teach it a job, such as picking up a sponge and dropping it in a cup: save a pose at every step, press Play, and watch the arm do it again and again on its own.

What you'll learn

  • How servos turn a pulse length into an angle, and how to drive four of them from one ESP32
  • Why motors need their own power supply, and why all the grounds must be joined
  • How the ESP32 can make its own Wi-Fi network and serve a control page to your phone
  • How to make servos move smoothly by taking many small steps
  • How "teach and repeat" programming works, the way factory robots are taught

What you need

  • ESP32 board, expansion board and USB cable (ESP32 IoT Starter Kit)
  • 4 SG90 servo motors with horns and screws: 1 from the ESP32 IoT Starter Kit and 3 more (MG90S metal-gear servos are stronger for the shoulder and elbow and fit the same holes)
  • A battery holder for 4 AA batteries with an on/off switch, with rechargeable NiMH batteries (4.8 V, best) or alkaline batteries (up to about 6.4 V when new, fine for short sessions), or a 5 V power supply that can give at least 2 A
  • Servo extension cables or male-to-female jumper wires: the cables of the elbow and gripper servos have to run all the way down the arm
  • 3 male-to-male jumper wires per servo if your servo plugs don't push into the breadboard (12 in total, ESP32 IoT Starter Kit)
  • The printed parts from the 3D printing box below (base, turntable, upper arm, forearm and jaw), or the cardboard version described there
  • A small screwdriver for the servo screws
  • A phone, tablet or laptop with Wi-Fi
  • Breadboard and jumper wires (ESP32 IoT Starter Kit)

Safety: the servos are small, but the gripper can pinch. Keep your fingers out of the jaw while it moves, and switch off the battery pack before you change any wires. If the arm hits something and a servo buzzes, move it away from the obstacle or switch off straight away: a stalled servo gets hot.

How the arm moves

Joint GPIO What it does Its range in the sketch
Base 13 Turns the whole arm left and right on the base 5° to 175°
Shoulder 25 Leans the upper arm forwards and back 20° to 160°
Elbow 26 Raises and lowers the forearm 20° to 160°
Gripper 27 Opens and closes the jaw against the fixed finger 0 % (closed) to 100 % (open)

With the base, shoulder and elbow, the gripper can reach many points around the arm, up to about 17 cm from the middle of the base; the gripper then holds the object. This is how real arms work too, only they often have six joints, so they can also tilt and twist the "hand".

Wiring

Breadboard wiring diagram: Build a mini ESP32 robot arm you control from your phone
Breadboard view. Check each connection against the wiring table below.

Build it step by step

Plug in all the parts first, then add the wires in this order. In each picture the new wires are bright and the ones from earlier steps are faded. Click a picture to see it larger.

Step 1 of 6: ESP32 GND to the − rail (the ESP32 itself runs on USB)
Step 1. ESP32 GND to the − rail (the ESP32 itself runs on USB)
Step 2 of 6: Base servo: brown → − rail, red → + rail, orange → GPIO 13
Step 2. Base servo: brown → − rail, red → + rail, orange → GPIO 13
Step 3 of 6: Shoulder servo: brown → − rail, red → + rail, orange → GPIO 25
Step 3. Shoulder servo: brown → − rail, red → + rail, orange → GPIO 25
Step 4 of 6: Elbow servo: brown → − rail, red → + rail, orange → GPIO 26
Step 4. Elbow servo: brown → − rail, red → + rail, orange → GPIO 26
Step 5 of 6: Gripper servo: brown → − rail, red → + rail, orange → GPIO 27
Step 5. Gripper servo: brown → − rail, red → + rail, orange → GPIO 27
Step 6 of 6: Battery pack last: + → + rail, − → − rail (keep it switched off)
Step 6. Battery pack last: + → + rail, − → − rail (keep it switched off)
From To
Each servo's brown wire (GND) Breadboard − rail (bottom)
Each servo's red wire (+) Breadboard + rail (bottom)
Base servo orange wire (signal) ESP32 GPIO 13
Shoulder servo orange wire ESP32 GPIO 25
Elbow servo orange wire ESP32 GPIO 26
Gripper servo orange wire ESP32 GPIO 27
Battery pack + (red) Breadboard + rail (bottom)
Battery pack − (black) Breadboard − rail (bottom)
ESP32 GND Breadboard − rail (bottom)

Why a battery pack? One servo draws up to a quarter of an amp while it moves, and about 0.7 A when it is blocked. Four of them together can pull 2 A in short bursts. The ESP32 gets its power from the USB cable, which can't give that much; if the servos shared it, the voltage would dip every time the arm moves and the ESP32 would restart. So the servos get their own power on the + rail, and the ESP32's 5V pin stays free. Only the grounds are joined: the servo signal is a voltage measured against GND, so the ESP32 and the servos must agree where "0 V" is. Without the shared ground the servos twitch or don't move at all.

Use the bottom rails only, and leave the battery pack switched off (or one battery out) until the sketch is uploaded. If you have an electrolytic capacitor of 100 µF or more, put it across the + and − rails near the servos (stripe to −, rated 10 V or more): it smooths out the bursts. On some breadboards the rails are split in the middle: if yours are, bridge the gap with a short wire. Keep the breadboard right next to the arm's base, so the servo cables reach.

Example screen for: Build a mini ESP32 robot arm you control from your phone
Example screen with sample values. Yours may look different.

The sketch

In the Arduino IDE, open Library Manager and install ESP32Servo by Kevin Harrington and WebSockets by Markus Sattler (search for "WebSockets" and pick the one by Markus Sattler).

Change ARM_PASSWORD to your own password of at least 8 characters, so nobody else can steer your arm. You don't need your home Wi-Fi: the arm makes its own network.

#include <WiFi.h>
#include <WebServer.h>
#include <WebSocketsServer.h>
#include <ESP32Servo.h>
#include <Preferences.h>

// ---------- settings ----------
const char* ARM_WIFI = "MiniRobotArm";       // the arm makes its own Wi-Fi network
const char* ARM_PASSWORD = "change-me-123";   // at least 8 characters
const bool ASSEMBLY = false;                  // true: all servos go to 90 degrees and stay there

// joints: 0 base, 1 shoulder, 2 elbow, 3 gripper
const char* NAMES[4] = {"Base", "Shoulder", "Elbow", "Gripper"};
const int PINS[4] = {13, 25, 26, 27};
const int MIN_DEG[4] = {5, 20, 20, 0};        // limits: make them smaller if parts bump into each other
const int MAX_DEG[4] = {175, 160, 160, 180};
const bool REVERSED[4] = {false, false, false, false};   // true: that servo turns the other way
const int HOME_DEG[3] = {90, 90, 90};         // base, shoulder, elbow
const int GRIP_CLOSED = 90;                   // the angle where the jaw touches the fixed finger
const int GRIP_OPEN = 140;                    // use 40 if the jaw turns the wrong way
const int MAX_STEPS = 30;                     // how many poses a program can hold
const unsigned long PAUSE_MS = 400;           // short stop at every pose while playing

WebServer server(80);
WebSocketsServer sockets(81);
Preferences memory;
Servo servos[4];

int now[4] = {90, 90, 90, 90};                // where each servo is
int target[4] = {90, 90, 90, 90};             // where each servo is going
int speed = 3;                                // degrees per 20 ms step (1 slow ... 6 fast)
uint8_t program[MAX_STEPS][4];                // the saved poses
int stepCount = 0, playIndex = 0;
bool playing = false, moved = true;
unsigned long lastMove = 0, lastSend = 0, arrivedAt = 0;

const char PAGE[] PROGMEM = R"rawliteral(<!doctype html><html><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no"><title>Mini robot arm</title>
<style>
body{margin:0;font-family:Arial,sans-serif;background:#F6F8FC;color:#1B3A7B;user-select:none}
header{background:#1B3A7B;color:#fff;padding:12px 16px;display:flex;justify-content:space-between;align-items:center}
header b{font-size:20px} #info{font-size:14px;opacity:.85}
.joint{background:#fff;margin:10px 12px;padding:10px 14px;border-radius:16px;box-shadow:0 2px 8px rgba(27,58,123,.08)}
.joint div{display:flex;justify-content:space-between;font-weight:bold;font-size:17px}
input[type=range]{width:100%;height:44px;accent-color:#DE544A}
.row{display:flex;gap:8px;margin:10px 12px}
button{flex:1;font-size:17px;font-weight:bold;padding:14px 6px;border:0;border-radius:14px;background:#1B3A7B;color:#fff}
button.red{background:#DE544A} button.light{background:#E3E8F2;color:#1B3A7B}
#steps{margin:4px 14px;font-size:15px}
</style></head><body>
<header><b>Mini robot arm</b><span id="info">Connecting...</span></header>
<div id="joints"></div>
<div class="row"><button class="light" onclick="grip(100)">Open</button><button class="light" onclick="grip(0)">Close</button><button class="light" onclick="send('home')">Home</button></div>
<div class="joint"><div><span>Speed</span><span id="sv">3</span></div><input type="range" id="speed" min="1" max="6" value="3"></div>
<div class="row"><button onclick="send('save')">Save pose</button><button id="play" class="red" onclick="playStop()">Play</button></div>
<div class="row"><button class="light" onclick="send('undo')">Undo last</button><button class="light" id="clear" onclick="clearAll()">Clear all</button></div>
<div id="steps">No poses saved yet.</div>
<script>
var NAMES = ['Base', 'Shoulder', 'Elbow', 'Gripper (% open)'], sock, playing = false, poses = 0, held = [false, false, false, false], pending = {}, clearTimer;
var box = document.getElementById('joints');
NAMES.forEach(function (n, i) {
  box.innerHTML += '<div class="joint"><div><span>' + n + '</span><span id="v' + i + '">-</span></div>' +
    '<input type="range" id="r' + i + '" min="0" max="' + (i == 3 ? 100 : 180) + '" value="' + (i == 3 ? 0 : 90) + '"></div>';
});
NAMES.forEach(function (n, i) {
  var r = document.getElementById('r' + i);
  r.oninput = function () { pending[i] = r.value; document.getElementById('v' + i).textContent = r.value; };
  r.onpointerdown = function () { held[i] = true; };
  r.onpointerup = r.onpointercancel = r.onlostpointercapture = r.onchange = function () { held[i] = false; pending[i] = r.value; };
});
document.getElementById('speed').oninput = function () { document.getElementById('sv').textContent = this.value; send('speed,' + this.value); };
setInterval(function () { for (var i in pending) send('s,' + i + ',' + pending[i]); pending = {}; }, 60);
function send(m) { if (sock && sock.readyState === 1) sock.send(m); }
function playStop() {
  if (!playing && poses == 0) { document.getElementById('steps').textContent = 'Save a pose first, then tap Play.'; return; }
  send(playing ? 'stop' : 'play');
}
function grip(p) { document.getElementById('r3').value = p; pending[3] = p; }
function clearAll() {
  var b = document.getElementById('clear');
  if (b.textContent === 'Tap again') { send('clear'); b.textContent = 'Clear all'; return; }
  b.textContent = 'Tap again'; clearTimeout(clearTimer);
  clearTimer = setTimeout(function () { b.textContent = 'Clear all'; }, 2500);
}
function connect() {
  sock = new WebSocket('ws://' + location.hostname + ':81/');
  sock.onopen = function () { document.getElementById('info').textContent = 'Connected'; };
  sock.onclose = function () { document.getElementById('info').textContent = 'Reconnecting...'; setTimeout(connect, 1000); };
  sock.onmessage = function (e) {
    var v = e.data.split(',');
    if (v[0] === 'a') {
      for (var i = 0; i < 4; i++) {
        document.getElementById('v' + i).textContent = v[i + 1];
        if (!held[i] && pending[i] === undefined) document.getElementById('r' + i).value = v[i + 5];
      }
    } else if (v[0] === 'n') {
      playing = v[2] === '1'; poses = +v[1];
      document.getElementById('play').textContent = playing ? 'Stop' : 'Play';
      document.getElementById('steps').textContent = v[1] == 0 ? 'No poses saved yet.' :
        (playing ? 'Playing pose ' + v[3] + ' of ' + v[1] : v[1] + ' poses saved (max ' + v[4] + ').');
      document.getElementById('speed').value = v[5]; document.getElementById('sv').textContent = v[5];
    }
  };
}
connect();
</script></body></html>)rawliteral";

int gripAngle(int percent) {                  // 0 % = closed, 100 % = wide open
  percent = constrain(percent, 0, 100);
  return GRIP_CLOSED + (GRIP_OPEN - GRIP_CLOSED) * percent / 100;
}

int gripPercent(int angle) {
  return constrain((int)lround((angle - GRIP_CLOSED) * 100.0 / (GRIP_OPEN - GRIP_CLOSED)), 0, 100);
}

void setTarget(int joint, int angle) {
  target[joint] = constrain(angle, MIN_DEG[joint], MAX_DEG[joint]);
}

void sendAngles() {                          // where the joints are, then where they are going
  char msg[64];
  snprintf(msg, sizeof(msg), "a,%d,%d,%d,%d,%d,%d,%d,%d", now[0], now[1], now[2], gripPercent(now[3]),
           target[0], target[1], target[2], gripPercent(target[3]));
  sockets.broadcastTXT(msg);
}

void sendStatus() {
  char msg[40];
  snprintf(msg, sizeof(msg), "n,%d,%d,%d,%d,%d", stepCount, playing ? 1 : 0, playIndex + 1, MAX_STEPS, speed);
  sockets.broadcastTXT(msg);
}

void saveProgram() {                          // keep the poses when the power is off
  memory.putBytes("poses", program, stepCount * 4);
  memory.putUChar("count", stepCount);
}

void stopPlaying() {                          // stop right where the arm is
  playing = false;
  for (int j = 0; j < 4; j++) target[j] = now[j];
}

void goToPose(int i) {
  for (int j = 0; j < 4; j++) setTarget(j, program[i][j]);
}

// Move every servo a little towards its target. Many small steps = smooth movement.
void moveServos() {
  for (int j = 0; j < 4; j++) {
    int diff = target[j] - now[j];
    if (diff == 0) continue;
    now[j] += constrain(diff, -speed, speed);
    servos[j].write(REVERSED[j] ? 180 - now[j] : now[j]);
    moved = true;
  }
}

bool arrived() {
  for (int j = 0; j < 4; j++) if (now[j] != target[j]) return false;
  return true;
}

void onSocket(uint8_t client, WStype_t type, uint8_t* payload, size_t length) {
  if (type == WStype_CONNECTED) {
    moved = true;
    sendStatus();
    return;
  }
  if (type != WStype_TEXT) return;
  String msg = String((char*)payload).substring(0, length);

  if (msg.startsWith("s,")) {                 // "s,joint,value" from a slider
    int joint = msg.substring(2, 3).toInt();
    int value = msg.substring(4).toInt();
    if (joint < 0 || joint > 3) return;
    if (playing) {
      playing = false;
      sendStatus();
    }
    setTarget(joint, joint == 3 ? gripAngle(value) : value);
  } else if (msg == "home") {
    playing = false;
    for (int j = 0; j < 3; j++) setTarget(j, HOME_DEG[j]);
    sendStatus();
  } else if (msg == "save" && stepCount < MAX_STEPS) {
    if (playing) stopPlaying();
    for (int j = 0; j < 4; j++) program[stepCount][j] = target[j];
    stepCount++;
    saveProgram();
    sendStatus();
  } else if (msg == "undo" && stepCount > 0) {
    if (playing) stopPlaying();
    stepCount--;
    saveProgram();
    sendStatus();
  } else if (msg == "clear") {
    stepCount = 0;
    playing = false;
    saveProgram();
    sendStatus();
  } else if (msg == "play" && stepCount > 0) {
    playing = true;
    playIndex = 0;
    arrivedAt = 0;
    goToPose(0);
    sendStatus();
  } else if (msg == "stop") {
    stopPlaying();
    sendStatus();
  } else if (msg.startsWith("speed,")) {
    speed = constrain(msg.substring(6).toInt(), 1, 6);
    sendStatus();
  }
}

// While playing: wait until every servo has arrived, pause a moment, then go to the next pose.
void playProgram(unsigned long t) {
  if (stepCount == 0) {
    playing = false;
    return;
  }
  if (!arrived()) return;
  if (arrivedAt == 0) arrivedAt = t;
  if (t - arrivedAt < PAUSE_MS) return;
  playIndex = (playIndex + 1) % stepCount;     // after the last pose, start again
  arrivedAt = 0;
  goToPose(playIndex);
  sendStatus();
}

void setup() {
  Serial.begin(115200);
  for (int j = 0; j < 4; j++) {
    servos[j].setPeriodHertz(50);               // standard servo signal: 50 pulses a second
    servos[j].attach(PINS[j], 500, 2400);       // pulse length for 0 and 180 degrees
    if (!ASSEMBLY && j < 3) now[j] = target[j] = HOME_DEG[j];
    if (!ASSEMBLY && j == 3) now[j] = target[j] = GRIP_CLOSED;
    servos[j].write(REVERSED[j] ? 180 - now[j] : now[j]);
    delay(300);                                 // one servo at a time: easier on the batteries
  }
  if (ASSEMBLY) {
    Serial.println("Assembly mode: all servos at 90 degrees. Fit the horns now.");
    while (true) delay(1000);
  }

  memory.begin("arm", false);
  stepCount = min((int)memory.getUChar("count", 0), MAX_STEPS);
  if (stepCount > 0) memory.getBytes("poses", program, sizeof(program));
  Serial.printf("%d saved poses loaded\n", stepCount);

  if (!WiFi.softAP(ARM_WIFI, ARM_PASSWORD)) {
    Serial.println("Wi-Fi failed: the password needs 8 to 63 characters");
  }
  server.on("/", []() { server.send_P(200, "text/html", PAGE); });
  server.begin();
  sockets.begin();
  sockets.onEvent(onSocket);
  Serial.printf("Join the Wi-Fi \"%s\", then open http://", ARM_WIFI);
  Serial.println(WiFi.softAPIP());
}

void loop() {
  server.handleClient();
  sockets.loop();
  unsigned long t = millis();
  if (t - lastMove >= 20) {                     // 50 small moves a second
    lastMove = t;
    moveServos();
  }
  if (playing) playProgram(t);
  if (moved && t - lastSend >= 100) {          // tell the phones where the arm is, 10 times a second
    lastSend = t;
    moved = false;
    sendAngles();
  }
}

Build the arm

A servo can only turn about 180°. If you fit a horn at a random position, a joint might reach the end of its range just where you need it. So the sketch first puts every servo in the middle, at 90°, and you build the arm around that position.

  1. Set ASSEMBLY = true near the top of the sketch and upload it. Switch on the battery pack: each servo turns to 90° and stays there. The Serial Monitor (115200 baud) says "Assembly mode"; if you opened it too late, press the EN (reset) button on the ESP32. Every servo goes into its part from the side without the horn recess.
  2. Base: drop the base servo into the tower from the top, with its shaft over the middle of the plate and the cable through the gap at the front. Fix it with its two screws.
  3. Turntable: press a single-arm horn into the recess under the turntable. Put the turntable on the base servo so that its wall runs parallel to the long 90° mark at the front of the base: the arm will reach out over that mark. Push it down and fix it with the horn screw through the small hole in the middle. Do this now: once the shoulder servo is in, you can't reach this screw any more.
  4. Shoulder: push the shoulder servo into the hole in the turntable's wall from the side without the two supports, shaft at the top, and fix it with its two screws.
  5. Elbow: drop the elbow servo into the far end of the upper arm, shaft at the very end. Press a horn into the recess under the other end, and press it onto the shoulder shaft so that the upper arm points straight up. Fix it with the horn screw.
  6. Gripper: drop the gripper servo into the forearm the same way. Fit a horn under its other end and press it onto the elbow shaft so that the forearm points straight forwards over the 90° mark, level with the table.
  7. Jaw: fit a horn under the jaw and press it onto the gripper shaft so that the jaw's finger lies flat against the fixed finger. Fix every horn with its screw.
  8. Lead the cables down along the arm with tape or small cable ties, leaving a loose loop at every joint so nothing pulls when the arm moves.
  9. Set ASSEMBLY = false and upload again.

Horns have teeth, so you may not get exactly straight up or exactly level. Pick the closest position; a few degrees off is fine. Screw the base plate to a board or put something heavy on its corners: a stretched-out arm tips easily.

Try it

  1. Switch on the battery pack and plug in the USB cable. The arm moves to its home pose: upper arm up, forearm forwards, jaw closed.
  2. On your phone, open the Wi-Fi settings and join MiniRobotArm with your password. If the phone says the network has no internet, choose to stay connected.
  3. Open http://192.168.4.1 in the browser. The page says "Connected" and shows four sliders.
  4. Drag each slider slowly and watch its joint follow. Tap Open and Close for the gripper, and Home to bring the base, shoulder and elbow back. Home leaves the gripper as it is, so the arm never drops what it holds.
  5. Teach it a job. Put a sponge or a scrunched-up paper ball in front of the arm and a cup to one side. Then make each pose and tap Save pose after each one: 1 above the ball with the jaw open, 2 lowered round the ball, 3 slide the Gripper slider down until the jaw just squeezes the ball (stop as soon as the servo hums), 4 lifted, 5 turned above the cup, 6 jaw open, 7 home.
  6. Tap Play. The arm goes from pose to pose, waits a moment at each one, and starts again after the last one. Use the Speed slider to make it faster or slower, and tap Stop to stop it where it is.
  7. Switch everything off and on again: the poses are still there, because the ESP32 keeps them in its flash memory.

If the jaw turns away from the fixed finger when you tap Close, or presses so hard that the servo hums, see the Troubleshooting section below. Every printed arm is a little different, and changing a number in the sketch is part of the job.

What you should see

Serial Monitor at 115200 baud showing 0 saved poses loaded and the MiniRobotArm Wi-Fi address http://192.168.4.1, the arm's home pose, and 7 taught poses from picking up a ball to dropping it in a cup

How it works

A servo pulse every 20 ms where 0.5 ms means 0°, 1.45 ms 90° and 2.4 ms 180°, beside a 3° every 20 ms staircase to a 120° target and a table of saved poses that Play steps through

A hobby servo is a small motor with a gearbox, a potentiometer that measures where the shaft is, and a little control circuit. It listens to one pulse every 20 ms (50 pulses a second). The length of the pulse is the angle: about 0.5 ms means 0°, about 2.4 ms means 180°, and anything in between is an angle in between. The control circuit keeps turning the motor until the potentiometer agrees with the pulse. The ESP32Servo library uses the ESP32's PWM hardware to make these pulses on four pins at once, so your sketch only has to say servos[j].write(angle).

If you sent the new angle straight to a servo, it would jump there at full speed: the arm would swing, the object would fly out of the gripper, and the battery would get a big current spike. So the sketch keeps two numbers for every joint: where it is (now) and where it should go (target). Fifty times a second, moveServos() moves each joint a few degrees closer to its target. At speed 3, that is 3° × 50 = 150° per second. All four joints move at the same time, so the movement looks smooth and natural.

With WiFi.softAP() the ESP32 becomes an access point, like a small Wi-Fi router, at the address 192.168.4.1. Your phone loads the control page from it once, and then the page opens a WebSocket: a connection that stays open, so messages can go both ways in a few milliseconds. A slider sends short messages such as s,1,120 (shoulder to 120°), no more than about 16 times a second, and the ESP32 sends back a,90,120,95,0 with the real angles. That is how the sliders on a second phone follow along, and why they move by themselves while the arm plays.

Save pose stores the four target angles as one row of the program table. Play sets the targets to row 1, waits until every joint has arrived, pauses for 0.4 s, then moves on to the next row. Factory robots are taught exactly like this, with a handheld control box called a teach pendant. The table is also saved with Preferences in the ESP32's flash memory, so it is still there after the power has been off.

Try this next

  • Add the joystick from the kit as a second controller: left and right turn the base, up and down move the shoulder, and pressing the button opens or closes the gripper.
  • Let the arm reach a point instead of setting angles: with the lengths of the upper arm (70 mm from shaft to shaft) and the forearm and the law of cosines from school maths, you can work out the shoulder and elbow angles for a given distance and height. This is called inverse kinematics.
  • Put an ultrasonic sensor next to the arm, as in the ultrasonic radar, and start the program only when something is placed in front of it, like a robot on a conveyor belt.
  • Store two or three programs (for example "sort", "wave" and "dance") and add a button for each one on the page.
  • Race a friend: who can teach the arm to stack two small boxes with the fewest poses?

Troubleshooting

  • The ESP32 restarts or the page disconnects when the arm moves: the servos are taking power from the ESP32. Check that no red servo wire goes to the ESP32, that the battery pack is on the + rail, and that the batteries are fresh.
  • The servos twitch or don't move, but the ESP32 is fine: the grounds are not joined. Check the wire from ESP32 GND to the − rail, and that the battery pack's black wire is on the same − rail.
  • One joint doesn't move: check its orange wire against the wiring table. Swap its plug with a joint that works: if the problem moves with the servo, the servo is broken.
  • The page doesn't open: check that the phone is still connected to MiniRobotArm (some phones leave a network without internet). Switching off mobile data for a moment helps. Type http://192.168.4.1 exactly, with http and not https.
  • A joint moves the wrong way: that is fine for the sliders, but if you want "right means right", set that joint to true in REVERSED and upload again. For the gripper, change GRIP_OPEN to 40 instead. After changing REVERSED, GRIP_CLOSED or GRIP_OPEN, tap Clear all and teach the poses again.
  • The gripper hums when it is closed: it is pushing against the fixed finger. Change GRIP_CLOSED a few degrees towards GRIP_OPEN until it just touches.
  • No MiniRobotArm network appears: look at the Serial Monitor. "Wi-Fi failed" means the password is shorter than 8 characters.
  • Parts bump into each other or a servo buzzes at the end of a slider: make the range smaller in MIN_DEG and MAX_DEG for that joint.
  • The arm sags or can't lift the object: use something lighter (under about 20 g), keep the forearm closer to the base, or use MG90S servos for the shoulder and elbow.
  • A printed part is too tight or too loose: printers differ by a few tenths of a millimetre. Sand or file a tight hole, and fix a loose part with a drop of hot glue or a strip of tape.

Build it yourself

Everything used in this lesson comes in one kit, with parts tested by our engineers.

ESP32 IoT Starter Kit | Arduino IDE Compatible | Free Tutorials $57.00

Shop the kit