Autonomous Underwater Claw
Closed-loop underwater gripper with automatic grab verification and retry.
Scope: APSC 101 course team project. Tools: Arduino, SolidWorks.
Overview
A gripper mechanism that detects, grabs, confirms, and releases an object underwater with no operator input. An Arduino reads distance from an ultrasonic sensor, drives a 180 degree servo to actuate the claw, and runs a state machine that verifies each grab actually lifted the object before committing, retrying automatically on failure.
How It Works
The system runs a four state machine:
- SEARCHING: waits for an object to enter grab range, within 10 cm.
- SETTLING: confirms the object stays in range for a short hold window before committing, so a momentary blip does not trigger a grab.
- GRABBING: closes the claw, then checks whether the measured distance rises past a lift confirmation threshold of 18 cm. If it does, the grab succeeded. If not, within the retry window, it reopens and returns to searching.
- HOLDING: keeps the claw closed until the object is released or lost, past 25 cm, then reopens.
The key design point is that it is closed loop. It does not assume a grab worked. It confirms the lift from sensor feedback and self corrects on failure. Distance readings are smoothed with a rolling average, and sampling runs at a fixed 50 ms interval using non blocking timing rather than blocking delays.
Design Decisions
- Replaced overlapping open loop trigger conditions with an explicit state machine to remove state chatter, where multiple branches previously fought over the servo near threshold boundaries.
- Spread the grab, lift confirm, and lost thresholds (10, 18, and 25 cm) well outside ultrasonic measurement jitter so the smoothed reading cannot bounce between states.
- Added lift confirmation as the feedback signal that makes the loop closed rather than fire and forget.
- Used a rolling average filter plus a fixed rate, non blocking sample loop for timing stability.
Fabrication
Sheet metal components were fabricated to ±2 mm tolerances across 12 joints. Interference envelopes were simulated in SolidWorks before fabrication, which cut material waste by 20% pre build. I developed the CAD and engineering drawings for what the team named QuadGrip.
Source Code
The full Arduino sketch is on GitHub, unmodified from what runs on the hardware.
claw.ino
#include <Servo.h>
Servo myServo;
// ---------------- Pins ----------------
const int servoPin = 8;
const int trigPin = 11;
const int echoPin = 10;
// ---------------- Servo positions ----------------
const int posOpen = 180;
const int posClose = 0;
// ---------------- Thresholds (cm) ----------------
// Spread wider than sonar jitter to avoid state chatter near boundaries.
const float GRAB_THRESHOLD = 10.0; // object close enough to attempt grab
const float LIFT_CONFIRM = 18.0; // distance proving the object was lifted
const float LOST_THRESHOLD = 25.0; // object gone / dropped: reopen and reset
// ---------------- Timing (ms) ----------------
const unsigned long HOLD_TIME = 300; // object must stay in range before grabbing
const unsigned long LIFT_WAIT = 1200; // window to confirm a successful lift
const unsigned long LOOP_PERIOD = 50; // fixed sample interval
const unsigned long ECHO_TIMEOUT = 25000; // us; caps pulseIn blocking
// ---------------- Averaging ----------------
#define AVG_SIZE 5
float distances[AVG_SIZE];
int idx = 0;
bool bufferFilled = false;
// ---------------- State machine ----------------
enum ClawState { SEARCHING, SETTLING, GRABBING, HOLDING };
ClawState state = SEARCHING;
unsigned long settleStart = 0;
unsigned long grabStart = 0;
unsigned long lastLoop = 0;
// ------------------------------------------------
// Rolling average. Returns false until the buffer has real data.
bool getAverageDistance(float newVal, float &out) {
distances[idx] = newVal;
idx = (idx + 1) % AVG_SIZE;
if (idx == 0) bufferFilled = true;
if (!bufferFilled) return false;
float sum = 0;
for (int i = 0; i < AVG_SIZE; i++) sum += distances[i];
out = sum / AVG_SIZE;
return true;
}
// ------------------------------------------------
// Returns distance in cm, or -1 on timeout (no echo).
float readSonar() {
digitalWrite(trigPin, LOW);
delayMicroseconds(2);
digitalWrite(trigPin, HIGH);
delayMicroseconds(10);
digitalWrite(trigPin, LOW);
long duration = pulseIn(echoPin, HIGH, ECHO_TIMEOUT);
if (duration == 0) return -1.0;
return duration * 0.034 / 2.0;
}
// ------------------------------------------------
void openClaw() { myServo.write(posOpen); }
void closeClaw() { myServo.write(posClose); }
// ------------------------------------------------
void setup() {
pinMode(trigPin, OUTPUT);
pinMode(echoPin, INPUT);
myServo.attach(servoPin);
openClaw();
Serial.begin(9600);
lastLoop = millis();
}
// ------------------------------------------------
void loop() {
unsigned long now = millis();
if (now - lastLoop < LOOP_PERIOD) return; // fixed-rate sampling
lastLoop = now;
float raw = readSonar();
if (raw < 0) return; // no echo this cycle, skip
float distance;
if (!getAverageDistance(raw, distance)) return; // wait for buffer to fill
Serial.print("State: ");
Serial.print(state);
Serial.print(" Dist: ");
Serial.println(distance);
switch (state) {
case SEARCHING:
// Wait for an object to enter grab range.
if (distance <= GRAB_THRESHOLD) {
state = SETTLING;
settleStart = now;
}
break;
case SETTLING:
// Confirm the object is stable in range before committing.
if (distance > GRAB_THRESHOLD) {
state = SEARCHING; // drifted out, abort
} else if (now - settleStart >= HOLD_TIME) {
Serial.println("Closing claw");
closeClaw();
state = GRABBING;
grabStart = now;
}
break;
case GRABBING:
// Closed-loop check: did the grip actually lift the object?
if (distance >= LIFT_CONFIRM) {
Serial.println("Lift confirmed");
state = HOLDING; // success
} else if (now - grabStart >= LIFT_WAIT) {
Serial.println("Grab failed, retrying");
openClaw();
state = SEARCHING; // failure, reopen and retry
}
break;
case HOLDING:
// Hold until the object is released or lost.
if (distance >= LOST_THRESHOLD) {
Serial.println("Object released, reopening");
openClaw();
state = SEARCHING;
}
break;
}
}