Cam Claw – a 3D Printed Robotic Grabber Equipped With a POV Camera

by gokux in Circuits > Robots

5269 Views, 74 Favorites, 0 Comments

Cam Claw – a 3D Printed Robotic Grabber Equipped With a POV Camera

imagxe1-9.jpg
Cam Claw – A 3D Printed Robotic Grabber Equipped With a POV Camera

The Cam Claw is designed to assist with a variety of outdoor activities. It can be employed to gently remove fruits and flowers located several feet up in a tree. plant , remove a shuttlecock wedged against a ceiling, or probe into dark and narrow places such as drains. You can see what the claw grabs in real time, courtesy of the incorporated camera, which presents the video feed right from the first-person perspective. It is also useful for nighttime activities or dark places thanks to the LED light.

The system is centred around an XIAO ESP32-S3 Sense microcontroller with an OV3660 camera. It uses a fully 3D printed servo-driven claw for gripping, and any mobile device for control over Wi-Fi. For this build, I mounted it on a 7-foot pole, but the lightweight design, along with the robust wireless connection, allows it to be mounted to poles over 20 feet without losing control or video feed.

Lightweight, compact, and easy to operate, the Cam Claw combines IoT-based wireless control, real-time video streaming, and mechanical gripping into a single outdoor-friendly tool that can be adapted for harvesting, object retrieval, or exploration.

So let's get into building

Note: There was a typo during the documentation of the app, "CAM CALW," which has been resolved in the program update.

Supplies

DSC04602.jpg

Designing in Fusion 360 and 3D Printing

Page 1.jpg

All of the components are designed using Fusion 360 and exported as STL files for 3D printing. I used PLA filament for the 3D printing and chose two different colors for better contrast. All the parts were printed with a layer height of 0.2 mm.

Custom PCB

DSC04590.JPG
Screenshot 2025-09-16 065132.png

The XIAO ESP32S3 SENSE lacks a screw hole, making it impossible to install directly in your project without a PCB. To solve this issue, I designed a general-purpose XAIO PCB and utilised a Seeed Fusion service to bring the project to life.

Do you want to bring your XIAO projects to life?

Seeed Studio is supporting makers worldwide through the Seeed Fusion XIAO Sponsorship Program. By joining, you can enjoy:

  1. 30% off Fusion PCBA services
  2. Global shipping and technical support
  3. A chance to turn your prototype into a real product

👉 Learn more and apply here: https://www.seeedstudio.com/blog/2025/05/28/turn-your-xiao-projects-into-reality-with-fusion-30-percent-off-pcba/

The PCB files are available in my GitHub https://github.com/gokuxmaker/XIAO-PCB-

PCB Assembly

Screenshot 2025-09-16 121031.png
Screenshot 2025-09-16 121113.png

Now, simply solder the XIAO onto the PCB. Additionally, solder the connections on the underside; this will help with thermal management. Finally, attach the provided heat sinks to the back side of both the camera and the PCB.

Code to XIAO

Screenshot 2025-09-16 121352.png

Connect to XIAO with the USB and upload the provided code to XIAO ESP32S3 using Arduino IDE

You can change the SSID and password if you need to, as well as the servo angles

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


// ====== CAMERA SETTINGS (XIAO ESP32S3 Sense + OV3660) ======
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 10
#define SIOD_GPIO_NUM 40
#define SIOC_GPIO_NUM 39


#define Y9_GPIO_NUM 48
#define Y8_GPIO_NUM 11
#define Y7_GPIO_NUM 12
#define Y6_GPIO_NUM 14
#define Y5_GPIO_NUM 16
#define Y4_GPIO_NUM 18
#define Y3_GPIO_NUM 17
#define Y2_GPIO_NUM 15
#define VSYNC_GPIO_NUM 38
#define HREF_GPIO_NUM 47
#define PCLK_GPIO_NUM 13


// ====== SERVO SETTINGS ======
Servo clawServo;
int servoPin = D0; // D0 on XIAO ESP32S3 = GPIO2
const int GRAB_ANGLE = -5;
const int RELEASE_ANGLE = 170;


// ====== WIFI SETTINGS ======
const char* ssid = "CamClaw";
const char* password = "12345678";


WebServer server(80);


// ====== MODERN HTML PAGE ======
String htmlPage = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no" />
<title>Cam Claw</title>
<style>
:root{
--bg:#f4f7fb; --card:#ffffff; --accent:#ff5a5a; --blue:#2a9dff; --text:#222;
}
html,body{height:100%; margin:0; font-family: 'Segoe UI', Arial, sans-serif; background:var(--bg);}
.wrap{max-width:820px; margin:20px auto; padding:20px; text-align:center; color:var(--text);}
h1{margin:10px 0 18px; font-size:26px; font-weight:600;}
.card{background:var(--card); border-radius:14px; padding:16px; box-shadow:0 6px 20px rgba(0,0,0,0.1);}
#cam { width:100%; max-width:720px; border-radius:10px; display:block; margin: 10px auto; box-shadow: 0 4px 12px rgba(0,0,0,0.2); }
.buttons{display:flex; gap:14px; justify-content:center; flex-wrap:wrap; margin-top:16px;}
.btn{
min-width:180px;
padding:20px 26px;
font-size:20px;
border-radius:14px;
border:none;
cursor:pointer;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
color:#fff;
transition:transform .12s ease, box-shadow .12s ease;
}
.grab{ background: linear-gradient(180deg,var(--accent),#ff2a2a); }
.release{ background: linear-gradient(180deg,var(--blue),#0077ff); }
.btn:active{ transform:scale(0.96); box-shadow:0 2px 6px rgba(0,0,0,0.2); }
#status{ color:#444; margin-top:12px; font-size:16px; font-weight:500;}
</style>
</head>
<body>
<div class="wrap">
<h1>CAM CALW</h1>
<div class="card">
<img id="cam" src="/capture?ts=0" alt="camera">
<div class="buttons">
<button class="btn grab" onclick="doAction('grab')">Grab</button>
<button class="btn release" onclick="doAction('release')">Release</button>
</div>
<div id="status">Status: idle</div>
</div>
</div>


<script>
const cam = document.getElementById('cam');
const status = document.getElementById('status');
setInterval(()=> { cam.src = '/capture?ts=' + Date.now(); }, 100);


function doAction(act) {
status.innerText = 'Status: ' + act + '...';
fetch('/' + act)
.then(r => r.json())
.then(j => status.innerText = 'Status: ' + j.status + ' (angle=' + j.position + ')')
.catch(e => status.innerText = 'Status: error');
}
</script>
</body>
</html>
)rawliteral";


// ====== HANDLERS ======
void handle_root() { server.send(200, "text/html", htmlPage); }


void handle_capture() {
camera_fb_t * fb = esp_camera_fb_get();
if (!fb) {
server.send(500, "text/plain", "Camera capture failed");
return;
}
server.sendHeader("Content-Type", "image/jpeg");
server.sendHeader("Cache-Control", "no-store");
WiFiClient client = server.client();
client.write((const char*)fb->buf, fb->len);
esp_camera_fb_return(fb);
}


void handle_grab() {
clawServo.write(GRAB_ANGLE);
String payload = String("{\"status\":\"Grabbed\",\"position\":") + GRAB_ANGLE + "}";
server.send(200, "application/json", payload);
}


void handle_release() {
clawServo.write(RELEASE_ANGLE);
String payload = String("{\"status\":\"Released\",\"position\":") + RELEASE_ANGLE + "}";
server.send(200, "application/json", payload);
}


// ====== SETUP ======
void setup() {
Serial.begin(115200);
delay(100);


// Enable camera power (needed on XIAO ESP32S3 Sense)
pinMode(21, OUTPUT);
digitalWrite(21, HIGH);
delay(10);


// Servo init
ESP32PWM::allocateTimer(0);
ESP32PWM::allocateTimer(1);
ESP32PWM::allocateTimer(2);
ESP32PWM::allocateTimer(3);
clawServo.setPeriodHertz(50);
clawServo.attach(servoPin, 500, 2400);
clawServo.write(RELEASE_ANGLE);


// Camera config
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sscb_sda = SIOD_GPIO_NUM;
config.pin_sscb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000;
config.pixel_format = PIXFORMAT_JPEG;
config.frame_size = FRAMESIZE_QVGA;
config.jpeg_quality = 10;
config.fb_count = 2;


if (esp_camera_init(&config) != ESP_OK) {
Serial.println("Camera init failed!");
while (true) delay(1000);
}
sensor_t * s = esp_camera_sensor_get();
s->set_vflip(s, 1);
s->set_hmirror(s, 1);
// Start AP
WiFi.softAP(ssid, password);
Serial.printf("AP started: SSID=%s IP=%s\n", ssid, WiFi.softAPIP().toString().c_str());


// Routes
server.on("/", HTTP_GET, handle_root);
server.on("/capture", HTTP_GET, handle_capture);
server.on("/grab", HTTP_GET, handle_grab);
server.on("/release", HTTP_GET, handle_release);
server.begin();
}


void loop() {
server.handleClient();
}

Wiring Diagram

CS.jpg

Here is the wiring diagram for the cam claw. Before starting the assembly, let's start placing the components inside the body.

Installing the PCB

Screenshot 2025-09-16 072508.png

Attached the PCB to the main body using M2 4mm screws.

LEDs

Screenshot 2025-09-16 072638.png

Cut the terminals of the 5mm LED and solder them together. Additionally, pair them with a 15-ohm resistor to regulate the LED current. You may also want to use some glue for added stability. It's advisable to remove the camera add-on during assembly, as it is somewhat fragile. We can reattach it once the assembly is complete. Make sure to connect all the LEDs in series.

MT3608 Voltage Booster

Screenshot 2025-09-16 072736.png

The MT3608 is a voltage booster that increases the voltage from 3.7V to 5V. Before assembling the MT3608, connect it to the battery and adjust the output voltage to 5V. We will use this 5V output to power the XIAO, LED, and servo. After setting the voltage, use some glue to securely attach the MT3608 to the main body.

Battery Holder

Screenshot 2025-09-16 072853.png
Screenshot 2025-09-16 124636.png

Secure the 18650 battery holder with glue; however, some models are available with screw holes for additional stability.

Power Switch

Screenshot 2025-09-16 075746.png

Insert the power switch located on the side of the main body.

Wifi Antenna

Screenshot 2025-09-16 125213.png
Screenshot 2025-09-16 075941.png

Connect the Wi-Fi antenna cable to the XIAO antenna port and secure it to the wall with adhesive.

Servo Motor

Screenshot 2025-09-16 080037.png
Screenshot 2025-09-16 080114.png

Screw the servo into the main body. I used the four screws that came with the servo for this.

Connecting Everything Together

Screenshot 2025-09-16 080248.png
Screenshot 2025-09-16 080304.png

Complete the wiring according to the wiring diagram

Camera Module

Screenshot 2025-09-16 080511.png
Screenshot 2025-09-16 080558.png

Reinsert the camera module and use glue to secure it in place for added stability.

Battery

Screenshot 2025-09-16 080819.png

I am using a 2000mAh 18650 battery to power everything. It is good to install the battery during operation and remove it for charging. The power switch will help you disconnect the battery from the circuit.

Gear Arm and Linkage Assembly

Screenshot 2025-09-16 132103.png
Screenshot 2025-09-16 083427.png

Attach the 2 linkages to the base using an M3x20mm screw. On the other side, attach the gear arm. And secure them with the nuts. Also, make sure not to tighten them

Arm Grippers Assembly

Screenshot 2025-09-16 084507.png
Screenshot 2025-09-16 084609.png

Attach both grippers to the linkage and gear arm using M3 x 20 mm screws.

Grippers Linkage Assembly

Screenshot 2025-09-16 132712.png
Screenshot 2025-09-16 132929.png

Connect the rest of the 4 linkages from the upper and lower sides to the base, and secure them with nuts


Pole Holder Assembly

ezgif-3f3e73eca97d73.gif
Screenshot 2025-09-16 095326.png

Put the pole holder on top of the base, and over the pole holder, place the main body. Secure using the two M3*20mm and M4*20mm

Servo Adapter to Gear Assembly

Screenshot 2025-09-16 095857.png

Attach the second metal adapter to the 3D printer using four M3*20mm screws


Servo Gear Assembly

Screenshot 2025-09-16 100125.png
Screenshot 2025-09-16 100206.png

Attach the gear to the servo. Ensure the arm is in the closed position and that the servo has stopped at the endstop in the clockwise direction. and secure the gear to the Servo using an M3x5mm screw


Top Lid

Screenshot 2025-09-16 100319.png

We just finished assembling the cam claw. Now we can close the top lid, which has a snap-fit design.

Testing

Adobe Express - Recording 2025-09-16 142701.gif

We have completed our main build. Power on the device, and the LED will light up. The grippers will automatically move to the open position. The Cam Calw will then create a Wi-Fi hotspot. Connect to this hotspot using the password 12345678.

Once you are connected to the Wi-Fi, open your browser and enter 192.168.4.1 in the address bar. This will take you to the control webpage, where you can see live video, as well as controls for grabbing and releasing objects. Additionally, you will find the status of the servo on this page.

Attaching to the Pole

Screenshot 2025-09-16 182249.png

Attach the 25mm, 7ft pole to the pole holder of the Cam Claw assembly. It’s recommended to drill a hole in the pole and secure the head in place with screws.

Phone Holder

Screenshot 2025-09-16 175920.png
DSC04662.JPG

I used a 7-foot pole for this project, but due to the wireless capability, you can also use a longer pole, such as a 20-foot one. I attached the phone holder to the handle using a 1/4-inch bolt and then secured it to the pole. If you have a loose fit, it's better to drill a hole in the pole and fix the handle in place with some screws.

Final Thought

bitmap.jpg

I only had an idea, but no clear picture in my mind of how it would turn out. We can start with a bad design, but we can still improve it. No matter how poorly your design looks in your mind, just start working from it like a stone that can be transformed into a great piece of art.

Trust the process!