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.
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:
| Step | Succeeds when | Signal |
|---|---|---|
| INIT | Arm and gripper respond, e-stop free, arm home, gripper open | services, encoders |
| APPROACH | Computed position is within tolerance of the target | encoders (forward kinematics) |
| LOWER-ARM | Current spikes and height is near the target ground level | current + encoders |
| GRASP | Max current and claw angle stable for 5 s before "closed" | current + encoders |
| EXTRACT | Arm is back at home height | encoders only |
| TRANSPORT | The drop-off position is reached | external signal |
| RELEASE | Max 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:
- Mimic joints: the gripper angle follows the forearm linearly. It only opened to 45° instead of 90°, because the real relation isn't linear.
- Gazebo constraints: enforce the loop in the physics engine. It worked partly, but oscillated at higher speeds.
- 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.
Data & AI/5 min read
Reading place codes out of 125 years of archive scans
OCR, OpenCV and fuzzy matching to sort Staatsbosbeheer's paper archive.