ðŸĪ– Build Your Own 3D Printed 6DOF Pick and Place Robot Arm Using ESP32

ðŸĪ– Build Your Own 3D Printed 6DOF Pick and Place Robot Arm Using ESP32

📅 By Dinesh | Tech DIY Projects | 2026


🚀 Introduction

Have you ever wanted to build a smart 3D printed robot arm that can pick and place objects wirelessly from your phone or laptop?

In this project, I created a 6 degree of freedom pick and place robot arm using:

✅ ESP32 for wireless control
✅ 3D printed robot arm structure
✅ 3 × MG995R servos for heavy joints
✅ 3 × SG60 servos for lighter joints
✅ Custom HTML web interface for control

This arm can move in multiple directions, rotate the base, bend the shoulder and elbow, control the wrist, and open or close the gripper.

Let’s dive in!


                             


🧠 Project Concept

Here’s how the system works:

User connects to the ESP32 Wi-Fi hotspot.

The browser opens a custom control page.

The base joystick controls the continuous rotation servo.

The other joints are controlled using compact pulley-style controls.

ESP32 receives the commands and moves the servos accordingly.

Done! The robot arm becomes a fully wireless pick and place system.


🧰 Components Used

ComponentQuantity
ESP32 DevKit1
MG995R Servo Motor3
SG60 Servo Motor3
3D Printed Robot Arm Parts1 Set
External 5V Power Supply1
Jumper WiresAs needed
Screws and Mounting PartsAs needed

🔌 Connections

Servo to ESP32 Pin Configuration

JointServo TypeESP32 GPIO
BaseMG995R 360°13
ShoulderMG995R14
ElbowMG995R27
Wrist PitchSG6026
Wrist RollSG6025
GripperSG6033

Important Notes

  • Use a separate 5V supply for all servos.
  • Do not power the servos directly from the ESP32.
  • Connect ESP32 GND and servo supply GND together.
  • The base is a continuous rotation servo, so it works with left / stop / right control.

📟 ESP32 Code

The ESP32 creates a web page, receives control commands, and drives the robot arm servos.

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

const char* ssid = "RobotArm-ESP32";
const char* password = "12345678";

WebServer server(80);
Servo servos[6];

// Pins
const int servoPins[6] = {13, 14, 27, 26, 25, 33};

// Current values
int servoValue[6] = {90, 90, 90, 90, 90, 40};

// Safe limits
const int servoMin[6] = {0, 15, 10, 20, 0, 20};
const int servoMax[6] = {180, 165, 170, 160, 180, 90};

// Reverse direction if needed
const bool invertServo[6] = {false, false, false, false, false, false};

// Base continuous servo tuning
int BASE_STOP = 90;
unsigned long lastBaseCmdMs = 0;
int lastBaseCmdValue = 90;

// ------------------ Helpers ------------------
int applyAngle(int i, int angle) {
  angle = constrain(angle, 0, 180);
  if (invertServo[i]) angle = 180 - angle;
  return constrain(angle, servoMin[i], servoMax[i]);
}

void writePosServo(int i, int angle) {
  servoValue[i] = constrain(angle, 0, 180);
  servos[i].write(applyAngle(i, servoValue[i]));
}

void writeBase(int value) {
  value = constrain(value, 0, 180);
  servoValue[0] = value;
  lastBaseCmdValue = value;
  lastBaseCmdMs = millis();
  servos[0].write(value);
}

void stopBase() {
  writeBase(BASE_STOP);
}

void moveSmoothTo(int s1, int s2, int s3, int s4, int s5, int stepDelayMs = 10) {
  int target[6] = {servoValue[0], s1, s2, s3, s4, s5};
  bool done = false;

  while (!done) {
    done = true;
    for (int i = 1; i < 6; i++) {
      if (servoValue[i] < target[i]) {
        servoValue[i]++;
        done = false;
      } else if (servoValue[i] > target[i]) {
        servoValue[i]--;
        done = false;
      }
      servos[i].write(applyAngle(i, servoValue[i]));
    }
    delay(stepDelayMs);
  }
}

// ------------------ Presets ------------------
void goHome() {
  stopBase();
  moveSmoothTo(90, 90, 90, 90, 40);
}

void openGripper() {
  writePosServo(5, 70);
}

void closeGripper() {
  writePosServo(5, 20);
}

void pickSequence() {
  openGripper();
  delay(200);
  moveSmoothTo(115, 120, 95, 90, 70);
  delay(150);
  moveSmoothTo(135, 140, 90, 90, 70);
  delay(200);
  closeGripper();
  delay(400);
  moveSmoothTo(110, 115, 95, 90, 20);
}

void placeSequence() {
  moveSmoothTo(115, 120, 95, 90, 20);
  delay(150);
  moveSmoothTo(135, 140, 90, 90, 20);
  delay(200);
  openGripper();
  delay(400);
  moveSmoothTo(110, 115, 95, 90, 70);
}

// ------------------ HTML UI ------------------
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ESP32 Robot Arm Control</title>
<style>
  :root{
    --bg1:#07111d;
    --bg2:#0d2237;
    --card:rgba(255,255,255,0.08);
    --line:rgba(255,255,255,0.12);
    --text:#eef5ff;
    --muted:#aabed6;
    --blue:#4aa3ff;
    --purple:#7c5cff;
    --green:#22c55e;
    --orange:#f97316;
    --shadow:0 18px 50px rgba(0,0,0,0.30);
  }
  *{box-sizing:border-box;}
  body{
    margin:0;
    min-height:100vh;
    font-family:Arial, Helvetica, sans-serif;
    color:var(--text);
    background:
      radial-gradient(circle at top left, rgba(74,163,255,0.16), transparent 24%),
      radial-gradient(circle at bottom right, rgba(124,92,255,0.16), transparent 30%),
      linear-gradient(135deg,var(--bg1),var(--bg2));
    padding:16px;
  }
  .wrap{
    max-width:1200px;
    margin:auto;
    display:grid;
    gap:14px;
  }
  .top{
    padding:18px 20px;
    border-radius:22px;
    background:linear-gradient(135deg, rgba(255,255,255,0.10), rgba(255,255,255,0.05));
    border:1px solid var(--line);
    box-shadow:var(--shadow);
    backdrop-filter:blur(14px);
  }
  h1{
    margin:0 0 6px;
    font-size:clamp(26px,3vw,40px);
  }
  .sub{
    margin:0;
    color:var(--muted);
    line-height:1.5;
    font-size:14px;
  }
  .status{
    display:flex;
    flex-wrap:wrap;
    gap:8px;
    margin-top:12px;
  }
  .pill{
    padding:7px 11px;
    border-radius:999px;
    background:rgba(255,255,255,0.08);
    border:1px solid var(--line);
    color:var(--muted);
    font-size:13px;
  }
  .grid{
    display:grid;
    grid-template-columns:320px 1fr;
    gap:14px;
  }
  @media(max-width:900px){
    .grid{ grid-template-columns:1fr; }
  }
  .card{
    padding:14px;
    border-radius:20px;
    background:var(--card);
    border:1px solid var(--line);
    box-shadow:var(--shadow);
    backdrop-filter:blur(14px);
  }
  .card h2{
    margin:0 0 12px;
    font-size:19px;
  }

  /* Base joystick */
  .baseArea{
    display:flex;
    flex-direction:column;
    align-items:center;
    gap:10px;
  }
  .joy{
    width:220px;
    height:220px;
    border-radius:50%;
    position:relative;
    background:radial-gradient(circle at 35% 35%, rgba(255,255,255,0.15), rgba(255,255,255,0.05));
    border:1px solid rgba(255,255,255,0.14);
    box-shadow:inset 0 0 26px rgba(0,0,0,0.24);
    touch-action:none;
    user-select:none;
  }
  .joy::before{
    content:"";
    position:absolute;
    inset:20px;
    border-radius:50%;
    border:1px dashed rgba(255,255,255,0.14);
  }
  .thumb{
    width:72px;
    height:72px;
    border-radius:50%;
    position:absolute;
    left:50%;
    top:50%;
    transform:translate(-50%,-50%);
    background:linear-gradient(135deg,var(--blue),var(--purple));
    box-shadow:0 12px 24px rgba(0,0,0,0.24);
    display:flex;
    align-items:center;
    justify-content:center;
    font-weight:700;
    letter-spacing:0.5px;
  }
  .state{
    font-size:18px;
    font-weight:700;
  }
  .small{
    color:var(--muted);
    font-size:13px;
    text-align:center;
    line-height:1.4;
  }
  .btnRow{
    width:100%;
    display:grid;
    grid-template-columns:repeat(3,1fr);
    gap:8px;
    margin-top:6px;
  }
  button{
    border:none;
    border-radius:14px;
    color:white;
    font-weight:700;
    cursor:pointer;
    padding:12px 12px;
    font-size:15px;
    transition:transform .12s ease, opacity .12s ease;
    box-shadow:0 10px 20px rgba(0,0,0,0.16);
  }
  button:active{ transform:translateY(1px); opacity:.92; }
  .bBlue{ background:linear-gradient(135deg,#3b82f6,#7c5cff); }
  .bGreen{ background:linear-gradient(135deg,#22c55e,#16a34a); }
  .bOrange{ background:linear-gradient(135deg,#f97316,#ea580c); }
  .bDark{ background:rgba(255,255,255,0.12); }

  /* Compact pulley controls */
  .jointGrid{
    display:grid;
    grid-template-columns:repeat(auto-fit, minmax(180px, 1fr));
    gap:10px;
  }
  .joint{
    padding:12px;
    border-radius:16px;
    background:rgba(255,255,255,0.06);
    border:1px solid rgba(255,255,255,0.08);
  }
  .jointTop{
    display:flex;
    justify-content:space-between;
    align-items:center;
    margin-bottom:10px;
    font-size:14px;
  }
  .jointTop strong{ font-size:14px; }
  .angle{
    color:var(--muted);
    font-size:13px;
  }
  .pulley{
    display:grid;
    grid-template-columns:42px 1fr 42px;
    gap:8px;
    align-items:center;
  }
  .pm{
    height:44px;
    padding:0;
    border-radius:12px;
    font-size:22px;
    line-height:44px;
  }
  .range{
    width:100%;
    accent-color:var(--blue);
  }
  .rangeBox{
    padding:8px 0 0;
  }
  .footerBtns{
    display:grid;
    grid-template-columns:repeat(auto-fit, minmax(110px,1fr));
    gap:8px;
  }
  .note{
    margin-top:10px;
    color:var(--muted);
    font-size:12px;
    line-height:1.5;
  }
</style>
</head>
<body>
  <div class="wrap">
    <div class="top">
      <h1>ESP32 Robot Arm Control</h1>
      <p class="sub">Base uses a live joystick. Shoulder, elbow, wrist pitch, wrist roll, and gripper use compact pulley controls for quick pick-and-place movement.</p>
      <div class="status">
        <div class="pill">AP: RobotArm-ESP32</div>
        <div class="pill">IP: 192.168.4.1</div>
        <div class="pill" id="netStatus">Status: Ready</div>
      </div>
    </div>

    <div class="grid">
      <div class="card">
        <h2>Base Joystick</h2>
        <div class="baseArea">
          <div class="joy" id="baseJoy">
            <div class="thumb" id="baseThumb">BASE</div>
          </div>
          <div class="state" id="baseState">STOP</div>
          <div class="small">Drag left/right. Release to stop.</div>
          <div class="btnRow">
            <button class="bBlue" id="leftBtn">LEFT</button>
            <button class="bGreen" id="stopBtn">STOP</button>
            <button class="bOrange" id="rightBtn">RIGHT</button>
          </div>
        </div>
      </div>

      <div class="card">
        <h2>Compact Servo Controls</h2>
        <div class="jointGrid">

          <div class="joint">
            <div class="jointTop"><strong>Shoulder</strong><span class="angle" id="v1">90°</span></div>
            <div class="pulley">
              <button class="pm bDark" onclick="stepServo(1,-1)">−</button>
              <input class="range" type="range" id="s1" min="15" max="165" value="90" oninput="setServo(1,this.value)">
              <button class="pm bDark" onclick="stepServo(1,1)">+</button>
            </div>
          </div>

          <div class="joint">
            <div class="jointTop"><strong>Elbow</strong><span class="angle" id="v2">90°</span></div>
            <div class="pulley">
              <button class="pm bDark" onclick="stepServo(2,-1)">−</button>
              <input class="range" type="range" id="s2" min="10" max="170" value="90" oninput="setServo(2,this.value)">
              <button class="pm bDark" onclick="stepServo(2,1)">+</button>
            </div>
          </div>

          <div class="joint">
            <div class="jointTop"><strong>Wrist Pitch</strong><span class="angle" id="v3">90°</span></div>
            <div class="pulley">
              <button class="pm bDark" onclick="stepServo(3,-1)">−</button>
              <input class="range" type="range" id="s3" min="20" max="160" value="90" oninput="setServo(3,this.value)">
              <button class="pm bDark" onclick="stepServo(3,1)">+</button>
            </div>
          </div>

          <div class="joint">
            <div class="jointTop"><strong>Wrist Roll</strong><span class="angle" id="v4">90°</span></div>
            <div class="pulley">
              <button class="pm bDark" onclick="stepServo(4,-1)">−</button>
              <input class="range" type="range" id="s4" min="0" max="180" value="90" oninput="setServo(4,this.value)">
              <button class="pm bDark" onclick="stepServo(4,1)">+</button>
            </div>
          </div>

          <div class="joint">
            <div class="jointTop"><strong>Gripper</strong><span class="angle" id="v5">40°</span></div>
            <div class="pulley">
              <button class="pm bDark" onclick="stepServo(5,-1)">−</button>
              <input class="range" type="range" id="s5" min="20" max="90" value="40" oninput="setServo(5,this.value)">
              <button class="pm bDark" onclick="stepServo(5,1)">+</button>
            </div>
          </div>

        </div>

        <div class="note">
          Use the pulley controls for quick fine adjustment. This keeps the page compact and easier to use on mobile.
        </div>

        <div class="footerBtns" style="margin-top:12px;">
          <button class="bBlue" onclick="preset('home')">Home</button>
          <button class="bGreen" onclick="preset('pick')">Pick</button>
          <button class="bOrange" onclick="preset('place')">Place</button>
          <button class="bDark" onclick="preset('open')">Open</button>
          <button class="bDark" onclick="preset('close')">Close</button>
          <button class="bDark" onclick="refreshState()">Refresh</button>
        </div>
      </div>
    </div>
  </div>

<script>
const netStatus = document.getElementById('netStatus');
const baseState = document.getElementById('baseState');
const baseJoy = document.getElementById('baseJoy');
const baseThumb = document.getElementById('baseThumb');

let lastBaseValue = 90;
let lastBaseSend = 0;

function setNet(t){ netStatus.innerText = t; }

async function fetchOK(url){
  const r = await fetch(url);
  return r.ok;
}

async function setServo(i, v){
  v = Math.max(0, Math.min(180, parseInt(v)));
  document.getElementById('v' + i).innerText = v + '°';
  try{
    await fetchOK(`/set?i=${i}&v=${v}`);
    setNet('Status: Connected');
  }catch(e){
    setNet('Status: Offline');
  }
}

function stepServo(i, delta){
  const s = document.getElementById('s' + i);
  const next = parseInt(s.value) + delta;
  s.value = next;
  setServo(i, next);
}

async function preset(name){
  try{
    setNet('Status: Running ' + name);
    await fetchOK(`/preset?name=${name}`);
    await refreshState();
    setNet('Status: Connected');
  }catch(e){
    setNet('Status: Offline');
  }
}

async function refreshState(){
  try{
    const res = await fetch('/state');
    const data = await res.json();
    for(let i=0;i<6;i++){
      const s = document.getElementById('s'+i);
      const v = document.getElementById('v'+i);
      if(s) s.value = data['s'+i];
      if(v) v.innerText = data['s'+i] + '°';
    }
    baseState.innerText = 'STOP';
    resetBaseThumb();
    setNet('Status: Connected');
  }catch(e){
    setNet('Status: Offline');
  }
}

async function sendBase(v){
  v = Math.max(0, Math.min(180, Math.round(v)));
  const now = Date.now();
  if(v === lastBaseValue && (now - lastBaseSend) < 35) return;

  lastBaseValue = v;
  lastBaseSend = now;

  const offset = v - 90;
  if(Math.abs(offset) < 4) baseState.innerText = 'STOP';
  else if(offset < 0) baseState.innerText = 'LEFT ' + Math.abs(offset);
  else baseState.innerText = 'RIGHT ' + Math.abs(offset);

  try{
    await fetchOK(`/base?v=${v}`);
    setNet('Status: Connected');
  }catch(e){
    setNet('Status: Offline');
  }
}

function resetBaseThumb(){
  baseThumb.style.transform = 'translate(-50%, -50%)';
}

function setupBaseJoystick(){
  let dragging = false;

  function updateBase(clientX, clientY){
    const rect = baseJoy.getBoundingClientRect();
    const cx = rect.left + rect.width / 2;
    const cy = rect.top + rect.height / 2;

    let dx = clientX - cx;
    let dy = clientY - cy;

    const radius = rect.width / 2 - 40;
    const dist = Math.sqrt(dx*dx + dy*dy);

    if(dist > radius){
      dx = dx * radius / dist;
      dy = dy * radius / dist;
    }

    baseThumb.style.transform = `translate(${dx}px, ${dy}px) translate(-50%, -50%)`;

    const xNorm = dx / radius;
    const dead = 0.14;

    if(Math.abs(xNorm) < dead){
      sendBase(90);
      return;
    }

    const sign = xNorm > 0 ? 1 : -1;
    const speed = (Math.abs(xNorm) - dead) / (1 - dead);
    const offset = 10 + Math.round(speed * 20);
    sendBase(90 + sign * offset);
  }

  function stopBaseJoy(){
    dragging = false;
    resetBaseThumb();
    sendBase(90);
  }

  baseJoy.addEventListener('pointerdown', (e) => {
    dragging = true;
    baseJoy.setPointerCapture(e.pointerId);
    updateBase(e.clientX, e.clientY);
  });

  baseJoy.addEventListener('pointermove', (e) => {
    if(!dragging) return;
    updateBase(e.clientX, e.clientY);
  });

  baseJoy.addEventListener('pointerup', stopBaseJoy);
  baseJoy.addEventListener('pointercancel', stopBaseJoy);
  baseJoy.addEventListener('pointerleave', stopBaseJoy);

  document.getElementById('leftBtn').addEventListener('click', () => sendBase(60));
  document.getElementById('stopBtn').addEventListener('click', () => sendBase(90));
  document.getElementById('rightBtn').addEventListener('click', () => sendBase(120));
}

window.addEventListener('load', () => {
  setupBaseJoystick();
  refreshState();
});
</script>
</body>
</html>
)rawliteral";

// ------------------ Web handlers ------------------
void handleRoot() {
  server.send_P(200, "text/html", index_html);
}

void handleSet() {
  if (!server.hasArg("i") || !server.hasArg("v")) {
    server.send(400, "text/plain", "Missing i or v");
    return;
  }

  int i = server.arg("i").toInt();
  int v = server.arg("v").toInt();

  if (i < 1 || i > 5) {
    server.send(400, "text/plain", "Servo index must be 1 to 5");
    return;
  }

  writePosServo(i, v);
  server.send(200, "text/plain", "OK");
}

void handleBase() {
  if (!server.hasArg("v")) {
    server.send(400, "text/plain", "Missing v");
    return;
  }

  int v = server.arg("v").toInt();
  v = constrain(v, 0, 180);

  writeBase(v);
  server.send(200, "text/plain", "OK");
}

void handleState() {
  String json = "{";
  for (int i = 0; i < 6; i++) {
    json += "\"s" + String(i) + "\":" + String(servoValue[i]);
    if (i < 5) json += ",";
  }
  json += "}";
  server.send(200, "application/json", json);
}

void handlePreset() {
  if (!server.hasArg("name")) {
    server.send(400, "text/plain", "Missing name");
    return;
  }

  String name = server.arg("name");

  if (name == "home") {
    goHome();
  } else if (name == "pick") {
    pickSequence();
  } else if (name == "place") {
    placeSequence();
  } else if (name == "open") {
    openGripper();
  } else if (name == "close") {
    closeGripper();
  } else {
    server.send(400, "text/plain", "Unknown preset");
    return;
  }

  server.send(200, "text/plain", "OK");
}

void setup() {
  Serial.begin(115200);

  for (int i = 0; i < 6; i++) {
    servos[i].setPeriodHertz(50);
    servos[i].attach(servoPins[i], 500, 2400);
  }

  stopBase();
  writePosServo(1, 90);
  writePosServo(2, 90);
  writePosServo(3, 90);
  writePosServo(4, 90);
  writePosServo(5, 40);

  WiFi.mode(WIFI_AP);
  WiFi.softAP(ssid, password);

  Serial.println();
  Serial.println("ESP32 Robot Arm Started");
  Serial.print("AP IP: ");
  Serial.println(WiFi.softAPIP());

  server.on("/", handleRoot);
  server.on("/set", handleSet);
  server.on("/base", handleBase);
  server.on("/state", handleState);
  server.on("/preset", handlePreset);
  server.begin();
}

void loop() {
  server.handleClient();

  if (millis() - lastBaseCmdMs > 400 && lastBaseCmdValue != BASE_STOP) {
    stopBase();
  }
}

Servo Control Logic

  • Base servo: continuous rotation control
  • Other 5 servos: angle-based position control
  • Web UI: joystick for base + compact controls for other joints

🌐 HTML Web Interface

The web page includes:

  • Base joystick control
  • Compact pulley controls for shoulder, elbow, wrist, and gripper
  • Home, Pick, Place, Open, Close buttons

This makes the interface clean, professional, and easy to use on mobile or laptop.


📌 Working Principle

Here is how the robot works:

  1. ESP32 starts in hotspot mode
  2. User connects to the Wi-Fi from phone or laptop
  3. The browser opens the control dashboard
  4. Base joystick rotates the arm left or right
  5. Other joints move using compact sliders/buttons
  6. Gripper opens and closes to complete pick and place action

ðŸŽŪ Control Features

Base Control

The base is controlled using a joystick because it is a 360° continuous servo.

  • Drag left → rotate left
  • Drag right → rotate right
  • Release → stop

Joint Control

The shoulder, elbow, wrist pitch, wrist roll, and gripper are controlled using compact pulley-style controls.

  • + button → increase angle
  • button → decrease angle
  • Slider → fine adjustment

🛠️ Build Process

First, I 3D printed all the robot arm parts and assembled the base, shoulder, elbow, wrist, and gripper section by section.

After assembly, I mounted the servos and checked each joint movement manually.

Then I connected the ESP32 and uploaded the code.

Finally, I opened the web interface and tested the full pick and place movement.

📞 Contact 3D Prints for Teraby : link

Looking for custom 3D printed projects, robotic parts, prototypes, or engineering models?

✅ High-quality 3D printing
✅ Custom robotic projects
✅ Prototype development
✅ ESP32 & Arduino-based builds
✅ STL to final product support

ðŸ“Đ DM for custom orders and project collaborations
🚀 Bring your ideas into reality with Teraby 3D Prints


🎉 Final Result

✅ 3D printed 6DOF robot arm built successfully
✅ Wireless ESP32 control working
✅ Base joystick control working smoothly
✅ Servo joints responding correctly
✅ Pick and place motion completed properly

This project is a great example of combining 3D printing, robotics, ESP32, and web control in one system.


❤️ Let’s Connect

Have doubts or want to build your own version?

I can help you with:

  • full code
  • wiring diagram
  • blog format
  • YouTube script
  • project description for Instagram or LinkedIn

Comments

Popular posts from this blog

ðŸŽŊ Build Your Own RFID Attendance System Using Arduino + ESP32 + Google Sheets

How to make a Rfid id tag to turn on the Motor

Restoring an RC Car from E-Waste – DIY Bluetooth-Controlled RC Car Using Arduino