DIY Acrylic Robot Manipulator Mechanical Arm
Introduction:
Robotics and automation are becoming essential in industries, education, and research. Understanding how robotic arms work helps students and hobbyists learn about mechanical design, motion control, and real-world automation systems.
Traditional robotic systems can be expensive and complex for beginners.
In this project, we build a DIY Acrylic Robot Manipulator Mechanical Arm (4-DOF) that:
- Provides 4 Degrees of Freedom (4-DOF) for flexible movement.
- Uses precision laser-cut acrylic parts for accurate assembly.
- Supports servo motor integration
- Allows hands-on experience in mechanical design and motion control.
- Can control through webserver.
Hardware Required:
- DIY Acrylic Robot Manipulator Mechanical Arm Kit
- ESP 32 development borad
- SG 90 9G Mini Micro Servo plastic gear 180 degrees
- PCA9685 16-Channel 12-Bit PWM/Servo Driver I2C Interface
- Power supply (6 volt)
Note: PWM servo motor driver library — Download
Code:
#include <Wire.h>
#include <Adafruit_PWMServoDriver.h>
#include <WiFi.h>
#include <WebServer.h>
// ─── WiFi Credentials ───────────────────────────────────────────────
const char* ssid = “Robot”;
const char* password = “12345678”;
// ─── PCA9685 Setup ──────────────────────────────────────────────────
Adafruit_PWMServoDriver pca = Adafruit_PWMServoDriver(0x40);
#define SERVO_FREQ 50
#define SERVO_MIN 102
#define SERVO_MAX 512
// ─── PCA9685 Channel Mapping ────────────────────────────────────────
#define BASE_CH 0
#define SHOULDER_CH 1
#define ELBOW_CH 2
#define GRIPPER_CH 3
// ─── Default Angles ─────────────────────────────────────────────────
int baseAngle = 90;
int shoulderAngle = 90;
int elbowAngle = 120;
int gripperAngle = 100;
WebServer server(80);
// ─── Angle to PCA9685 Pulse ─────────────────────────────────────────
int angleToPulse(int angle) {
return map(angle, 0, 180, SERVO_MIN, SERVO_MAX);
}
void setServo(uint8_t channel, int angle) {
pca.setPWM(channel, 0, angleToPulse(angle));
}
// ─── Build & Send HTML Page ─────────────────────────────────────────
void handleRoot() {
String html = “”;
html += “<!DOCTYPE html><html lang=’en’><head>”;
html += “<meta charset=’UTF-8’/>”;
html += “<meta name=’viewport’ content=’width=device-width, initial-scale=1.0’/>”;
html += “<title>ESP32 Based 4DOF Robotic Arm</title>”;
html += “<link href=’https://fonts.googleapis.com/css2?family=Orbitron:wght@400;600;800&family=Rajdhani:wght@300;400;500… rel=’stylesheet’/>”;
html += “<style>”;
html += “:root{–c1:#B8E3E9;–c2:#93B1B5;–c3:#4F7C82;–c4:#0B2E33;}”;
html += “*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}”;
html += “body{background:var(–c4);color:var(–c1);font-family:’Rajdhani’,sans-serif;min-height:100vh;padding:20px;”;
html += “background-image:radial-gradient(ellipse at 20% 10%,rgba(79,124,130,0.3) 0%,transparent 50%),”;
html += “radial-gradient(ellipse at 80% 90%,rgba(147,177,181,0.15) 0%,transparent 50%),”;
html += “repeating-linear-gradient(0deg,transparent,transparent 40px,rgba(184,227,233,0.02) 40px,rgba(184,227,233,0.02) 41px),”;
html += “repeating-linear-gradient(90deg,transparent,transparent 40px,rgba(184,227,233,0.02) 40px,rgba(184,227,233,0.02) 41px);}”;
html += “.wrapper{max-width:620px;margin:0 auto;}”;
html += “header{text-align:center;padding:36px 0 28px;position:relative;}”;
html += “header::after{content:”;display:block;width:80px;height:2px;background:linear-gradient(90deg,transparent,var(–c3),transparent);margin:16px auto 0;}”;
html += “h1{font-family:’Orbitron’,monospace;font-size:clamp(1.1rem,4vw,1.6rem);font-weight:800;letter-spacing:0.05em;color:var(–c1);text-shadow:0 0 30px rgba(184,227,233,0.4);line-height:1.3;}”;
html += “.subtitle{font-size:0.85rem;color:var(–c3);letter-spacing:0.2em;text-transform:uppercase;margin-top:6px;font-weight:500;}”;
html += “.joint-card{background:linear-gradient(135deg,rgba(79,124,130,0.15),rgba(11,46,51,0.6));border:1px solid rgba(147,177,181,0.25);border-radius:12px;padding:22px 24px 20px;margin-bottom:16px;position:relative;overflow:hidden;backdrop-filter:blur(4px);transition:border-color 0.3s,box-shadow 0.3s;animation:slideUp 0.5s ease both;}”;
html += “.joint-card::before{content:”;position:absolute;top:0;left:0;width:4px;height:100%;background:linear-gradient(180deg,var(–c1),var(–c3));border-radius:12px 0 0 12px;opacity:0.7;}”;
html += “.joint-card:hover{border-color:rgba(184,227,233,0.45);box-shadow:0 8px 32px rgba(0,0,0,0.3);}”;
html += “.joint-card:nth-child(1){animation-delay:0.05s;}.joint-card:nth-child(2){animation-delay:0.15s;}.joint-card:nth-child(3){animation-delay:0.25s;}.joint-card:nth-child(4){animation-delay:0.35s;}”;
html += “.joint-title{font-family:’Orbitron’,monospace;font-size:0.9rem;font-weight:600;letter-spacing:0.12em;text-align:center;color:var(–c1);margin-bottom:18px;text-transform:uppercase;}”;
html += “.slider-row{display:grid;grid-template-columns:70px 1fr 70px;align-items:center;gap:10px;margin-bottom:12px;}”;
html += “.label{font-size:0.75rem;font-weight:600;letter-spacing:0.1em;text-transform:uppercase;color:var(–c2);}”;
html += “.label.left{text-align:right;}.label.right{text-align:left;}”;
html += “input[type=range]{-webkit-appearance:none;appearance:none;width:100%;height:6px;border-radius:3px;background:linear-gradient(90deg,var(–c3) var(–fill,50%),rgba(147,177,181,0.2) var(–fill,50%));outline:none;cursor:pointer;}”;
html += “input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:20px;height:20px;border-radius:50%;background:var(–c1);border:3px solid var(–c4);box-shadow:0 0 10px rgba(184,227,233,0.5),0 0 0 2px var(–c3);cursor:pointer;transition:transform 0.15s;}”;
html += “input[type=range]::-webkit-slider-thumb:hover{transform:scale(1.2);}”;
html += “.angle-display{display:flex;align-items:center;justify-content:center;gap:8px;}”;
html += “.angle-label{font-size:0.72rem;color:var(–c3);letter-spacing:0.08em;text-transform:uppercase;}”;
html += “.angle-value{font-family:’Orbitron’,monospace;font-size:1.05rem;font-weight:600;color:var(–c1);min-width:48px;text-align:center;background:rgba(79,124,130,0.2);border:1px solid rgba(147,177,181,0.3);border-radius:6px;padding:3px 10px;}”;
html += “.angle-unit{font-size:0.72rem;color:var(–c3);}”;
html += “.status-bar{text-align:center;padding:10px;margin-top:6px;font-size:0.72rem;letter-spacing:0.15em;text-transform:uppercase;color:var(–c3);}”;
html += “.dot{display:inline-block;width:6px;height:6px;border-radius:50%;background:#4CAF50;margin-right:6px;animation:blink 2s infinite;vertical-align:middle;}”;
html += “@keyframes blink{0%,100%{opacity:1;}50%{opacity:0.4;}}”;
html += “@keyframes slideUp{from{opacity:0;transform:translateY(20px);}to{opacity:1;transform:translateY(0);}}”;
html += “</style></head><body>”;
html += “<div class=’wrapper’>”;
html += “<header><h1>ESP32 Based 4DOF<br/>Robotic Arm</h1>”;
html += “<p class=’subtitle’>PCA9685 | Servo Control</p></header>”;
// Base card
html += “<div class=’joint-card’><div class=’joint-title’>Base</div>”;
html += “<div class=’slider-row’><span class=’label left’>Left</span>”;
html += “<input type=’range’ id=’base’ min=’0′ max=’180′ value='” + String(baseAngle) + “‘ oninput=’sendAngle(\”base\”,this.value)’ onchange=’sendAngle(\”base\”,this.value)’/>”;
html += “<span class=’label right’>Right</span></div>”;
html += “<div class=’angle-display’><span class=’angle-label’>Current Angle</span>”;
html += “<span class=’angle-value’ id=’baseVal’>” + String(baseAngle) + “</span><span class=’angle-unit’>deg</span></div></div>”;
// Shoulder card
html += “<div class=’joint-card’><div class=’joint-title’>Shoulder</div>”;
html += “<div class=’slider-row’><span class=’label left’>Backward</span>”;
html += “<input type=’range’ id=’shoulder’ min=’45’ max=’165′ value='” + String(shoulderAngle) + “‘ oninput=’sendAngle(\”shoulder\”,this.value)’ onchange=’sendAngle(\”shoulder\”,this.value)’/>”;
html += “<span class=’label right’>Forward</span></div>”;
html += “<div class=’angle-display’><span class=’angle-label’>Current Angle</span>”;
html += “<span class=’angle-value’ id=’shoulderVal’>” + String(shoulderAngle) + “</span><span class=’angle-unit’>deg</span></div></div>”;
// Elbow card
html += “<div class=’joint-card’><div class=’joint-title’>Elbow</div>”;
html += “<div class=’slider-row’><span class=’label left’>Down</span>”;
html += “<input type=’range’ id=’elbow’ min=’80’ max=’160′ value='” + String(elbowAngle) + “‘ oninput=’sendAngle(\”elbow\”,this.value)’ onchange=’sendAngle(\”elbow\”,this.value)’/>”;
html += “<span class=’label right’>Up</span></div>”;
html += “<div class=’angle-display’><span class=’angle-label’>Current Angle</span>”;
html += “<span class=’angle-value’ id=’elbowVal’>” + String(elbowAngle) + “</span><span class=’angle-unit’>deg</span></div></div>”;
// Gripper card
html += “<div class=’joint-card’><div class=’joint-title’>Gripper</div>”;
html += “<div class=’slider-row’><span class=’label left’>Close</span>”;
html += “<input type=’range’ id=’gripper’ min=’85’ max=’120′ value='” + String(gripperAngle) + “‘ oninput=’sendAngle(\”gripper\”,this.value)’ onchange=’sendAngle(\”gripper\”,this.value)’/>”;
html += “<span class=’label right’>Open</span></div>”;
html += “<div class=’angle-display’><span class=’angle-label’>Current Angle</span>”;
html += “<span class=’angle-value’ id=’gripperVal’>” + String(gripperAngle) + “</span><span class=’angle-unit’>deg</span></div></div>”;
html += “<div class=’status-bar’><span class=’dot’></span>Connected to ESP32</div>”;
html += “</div>”;
// JavaScript — no ‘function’ keyword issue since it’s inside a String
html += “<script>”;
html += “var mins={base:0,shoulder:45,elbow:80,gripper:85};”;
html += “var maxs={base:180,shoulder:165,elbow:160,gripper:120};”;
html += “var updateSlider=function(id,val){“;
html += ” var mn=mins[id],mx=maxs[id];”;
html += ” var pct=((val-mn)/(mx-mn))*100;”;
html += ” document.getElementById(id).style.setProperty(‘–fill’,pct+’%’);”;
html += ” document.getElementById(id+’Val’).textContent=val;”;
html += “};”;
html += “var sendAngle=function(joint,val){“;
html += ” val=parseInt(val);”;
html += ” updateSlider(joint,val);”;
html += ” var xhr=new XMLHttpRequest();”;
html += ” xhr.open(‘GET’,’/set?joint=’+joint+’&angle=’+val,true);”;
html += ” xhr.send();”;
html += “};”;
html += “window.onload=function(){“;
html += ” updateSlider(‘base’,” + String(baseAngle) + “);”;
html += ” updateSlider(‘shoulder’,” + String(shoulderAngle) + “);”;
html += ” updateSlider(‘elbow’,” + String(elbowAngle) + “);”;
html += ” updateSlider(‘gripper’,” + String(gripperAngle) + “);”;
html += “};”;
html += “</script></body></html>”;
server.send(200, “text/html”, html);
}
// ─── /set Handler ───────────────────────────────────────────────────
void handleSet() {
if (!server.hasArg(“joint”) || !server.hasArg(“angle”)) {
server.send(400, “text/plain”, “Missing parameters”);
return;
}
String joint = server.arg(“joint”);
int angle = server.arg(“angle”).toInt();
if (joint == “base”) {
angle = constrain(angle, 0, 180);
baseAngle = angle;
setServo(BASE_CH, angle);
} else if (joint == “shoulder”) {
angle = constrain(angle, 45, 165);
shoulderAngle = angle;
setServo(SHOULDER_CH, angle);
} else if (joint == “elbow”) {
angle = constrain(angle, 80, 160);
elbowAngle = angle;
setServo(ELBOW_CH, angle);
} else if (joint == “gripper”) {
angle = constrain(angle, 85, 120);
gripperAngle = angle;
setServo(GRIPPER_CH, angle);
} else {
server.send(400, “text/plain”, “Unknown joint”);
return;
}
server.send(200, “text/plain”, “OK”);
Serial.printf(“[SET] %s -> %d deg\n”, joint.c_str(), angle);
}
// ─── Setup ──────────────────────────────────────────────────────────
void setup() {
Serial.begin(115200);
// PCA9685 init — SDA=GPIO21, SCL=GPIO22 by default on ESP32
Wire.begin();
pca.begin();
pca.setOscillatorFrequency(27000000);
pca.setPWMFreq(SERVO_FREQ);
delay(10);
// Move servos to default positions
setServo(BASE_CH, baseAngle);
setServo(SHOULDER_CH, shoulderAngle);
setServo(ELBOW_CH, elbowAngle);
setServo(GRIPPER_CH, gripperAngle);
Serial.println(“[PCA9685] Servos initialised”);
// WiFi connect
Serial.printf(“[WiFi] Connecting to %s”, ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(“.”);
}
Serial.printf(“\n[WiFi] Connected! Open browser: http://%s\n”, WiFi.localIP().toString().c_str());
// Register routes
server.on(“/”, handleRoot);
server.on(“/set”, handleSet);
server.begin();
Serial.println(“[HTTP] Web server started”);
}
// ─── Loop ───────────────────────────────────────────────────────────
void loop() {
server.handleClient();
}
Frequently Asked Questions (FAQs):
- What does 4 DOF mean in this robotic arm?
Four Degrees of Freedom (DOF) means the arm can move in four independent directions or joints. - How are the servo motors controlled in this project?
Servo motors are controlled when the ESP32 receives commands from the web server and sends them via I2C to the PCA9685, which generates the PWM signals to move the servos. - How does the web server control the robotic arm?
The ESP32 hosts a web server that sends servo angle values through HTTP requests to control each joint. - Why is the servo driver used instead of connecting servos directly to ESP32?
PCA9685 16Channel 12Bit PWM/Servo Driver allows precise PWM control and supports multiple servos without overloading the ESP32 pin. - Is this suitable for engineering projects?
Yes. It demonstrates:
- Embedded Systems (ESP32 based control)
- IoT & Web-Based Wireless Control
- PWM-Based Servo Motor Control
- Robotics and Mechanical Manipulation
- I2C interface
Applications of the Project:
- Educational Robotics Learning Platform
- Remote Robot Control using IoT
- Prototype for Automated Assembly Systems
- Motion Control learning
At Zbotic, we support makers, students, and innovators with practical robotics kits, electronic modules, and DIY learning products designed for hands-on experimentation. This DIY Acrylic Robot Manipulator Mechanical Arm is ideal for understanding motion control, servo integration, and basic robotic mechanisms in a simple build format.
You can also explore related components such as servo motors, controller boards, jumper wires, robotic grippers, power modules, and DIY electronics accessories to expand your project further.
👉 Explore more robotics kits, electronic components, and DIY parts at zbotic.in
Add comment