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.

Bench test of the claw completing a grab.

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.

SolidWorks assembly render of the claw mechanism, isometric view, showing the Arduino Uno, ultrasonic sensor, and sheet metal frame legs.
SolidWorks assembly render, isometric view.
SolidWorks assembly render of the claw mechanism, isometric view, showing the servo motor, ultrasonic sensor, and sheet metal frame legs.
SolidWorks assembly render, isometric view.
Engineering drawing of the claw frame legs, dimensioned in millimeters, with hole callouts and position tolerance for the leg assembly.
Claw frame assembly drawing.

Source Code

The full Arduino sketch is on GitHub, unmodified from what runs on the hardware.

View on GitHub

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;
  }
}