← All articles

Robotics/7 min read

Teaching a robot gripper to check its own work

How I built the gripper software for the Pieperkieker potato robot in ROS 2 and Python, with a seven-step state machine, Gazebo simulation and 173 unit tests.

Mohamad.B Alhakim

The Pieperkieker is an autonomous robot built at Hanze Makerspace. It drives over a potato field with GPS, finds plants that are sick, and should pull them out before the disease spreads. When I joined for my graduation internship, finding the plants already worked. Removing them didn't: there was no software that could drive the gripper and tell whether a grab had actually worked.

That became my project. I spent 2026 building the gripper software in ROS 2 and Python, testing it in a Gazebo simulation, and making sure it can move to the real arm without rewriting the logic.

The hard part: a gripper with no sense of touch

Most gripper tutorials assume force sensors or tactile pads. This one has neither. The only feedback is motor current and absolute encoders (the joint angles).

So "did the grab work?" becomes a question you answer indirectly:

  • When the claws close on a plant, the current rises and the claw angle stops before it reaches the fully closed value.
  • When the claws close on nothing, the angle goes all the way to closed.
  • When the arm lowers onto the soil, the current jumps the moment it touches the ground.

Current on its own turned out to be unreliable for some steps. While pulling a plant out, for example, the current sits at its maximum the whole time. That's why every step uses the encoders to confirm the result, and only uses current where it really means something.

Seven steps, each with a postcondition

Before writing code I did a small literature study on workflow architectures for grippers: eight sources, each read with the same four questions. Which steps? Which sensor data? When do you move on? What happens on a failure? That led to a state machine with seven steps:

INIT → APPROACH → LOWER-ARM → GRASP → EXTRACT → TRANSPORT → RELEASE

One cycle handles one plant. The key rule is that a step only succeeds when its postcondition is confirmed with sensor data, not when the command has been sent:

StepSucceeds whenSignal
INITArm and gripper respond, e-stop free, arm home, gripper openservices, encoders
APPROACHComputed position is within tolerance of the targetencoders (forward kinematics)
LOWER-ARMCurrent spikes and height is near the target ground levelcurrent + encoders
GRASPMax current and claw angle stable for 5 s before "closed"current + encoders
EXTRACTArm is back at home heightencoders only
TRANSPORTThe drop-off position is reachedexternal signal
RELEASEMax current while opening and angle at "open"current + encoders

"Is it still moving?" is answered by checking whether the encoder value is still changing over about five seconds. The check loop runs at a fixed interval, so that's just a counter, not a separate timer.

Why I wrote my own state machine

I looked at SMACH and at Behavior Trees (py_trees). Behavior Trees are great when a robot has to keep choosing which task matters most right now. This gripper doesn't: it always runs the same steps in the same order, and for each failure it's known in advance where to go back to. The one thing that always takes priority, the emergency stop, is checked separately in every step.

So I wrote a small state machine myself, and borrowed one idea from Behavior Trees: inside a step there are only two kinds of building blocks.

  • A CHECK asks exactly one question about sensor data and never moves anything.
  • A COMMAND sends exactly one instruction.

If a step needs to check a current spike and a position, that's two checks in a row. It sounds strict, but it meant every box in the flowcharts maps to one piece of code, so you can put the diagram next to the source and read them together.

Every failure goes somewhere

Each step ends in one of three outcomes:

  • success: go to the next step,
  • recover: return to a known safe state and try again,
  • operator: stop the cycle with a specific error label.

There is deliberately no plain "failed". If the arm hits something during APPROACH, it goes home and retries with an offset. If the soil holds the plant during EXTRACT, it always opens the claws before moving the arm. Retries are capped (three per step, and a shared counter for GRASP and EXTRACT per plant) so the robot can't loop forever between grabbing and pulling. When recovery runs out, the operator gets a specific label instead of a vague error, like GRIJPER_VAST (gripper stuck) or APPROACH_BOTSING (collision while approaching).

Making it hardware-independent

The software had to be developed in simulation but eventually run on a real arm driven through an Arduino layer. So the states never talk to ROS or Gazebo directly. They only use one abstract RobotInterface: read angles, position, current, voltage and e-stop; send home, move, lower, lift, open, close and stop.

There are two implementations today:

  • SimRobot, a tiny fake robot without ROS, used by the unit tests;
  • GazeboRobot, the adapter to the simulation.

For the real arm, only a third implementation is needed. The state logic doesn't change. All thresholds, tolerances, timeouts and retry limits live in one YAML file, marked as provisional, so they can be calibrated on the real hardware without touching code.

Getting the arm into Gazebo

There was no URDF model of the arm, so I built one from the mechanical team's SolidWorks assembly. The automatic export produced a broken model, so the coordinate frames, links and joint axes had to be set up by hand.

Then a bigger problem showed up: the arm has a closed kinematic loop (one forearm is linked to the gripper), and URDF only supports trees. I tried three approaches:

  1. Mimic joints: the gripper angle follows the forearm linearly. It only opened to 45° instead of 90°, because the real relation isn't linear.
  2. Gazebo constraints: enforce the loop in the physics engine. It worked partly, but oscillated at higher speeds.
  3. Separate chains: simulate both arms as independent chains and drive them together.

The third one won. Inverse kinematics computes the angles for both arms for every target and sends them in one command, so they move together like the real linked arm.

Testing: 173 tests and ten scenarios

I built the logic test-first with pytest. Every check, every state and every route through the flowcharts has at least one test. Because the tests use SimRobot and an injectable clock, they run without ROS and without waiting: 173 tests in 0.6 seconds, with 100% coverage of the logic.

Some things Gazebo can't simulate realistically, like motor current or whether a grab physically holds. For those I wrote ten scenarios, each forcing one specific route through the flowcharts: a clean run, a collision, an obstacle while lowering, a missed grab, a plant stuck in the soil, a gripper stuck while releasing, and so on. All ten follow the expected route in Gazebo and end with the expected result. A full successful cycle takes about 17 seconds.

To demo and debug it, I added a small PyQt5 control panel (pick a plant, start and stop a cycle, trigger the e-stop) and CSV logging: one file per cycle, one row per check, so you can replay exactly which route the software took.

What I'd tell myself at the start

  • Write the flowcharts first, and get them approved. Rebuilding the states from approved flowcharts was faster than fixing the earlier code, which had three bugs Gazebo exposed straight away (one was open and close being swapped).
  • Put the hardware behind an interface on day one. It's what made 173 fast tests possible and what will make the move to the real arm cheap.
  • Don't trust one signal. Pairing current with encoder data is what made the checks reliable.

The next step is the real-hardware implementation of the interface and calibrating the thresholds on the actual arm. The logic is ready for it.

Blog / Keep reading

Related articles.

Teaching a robot gripper to check its own work - Mohamad Bashar Alhakim