
Autonomous Strawberry Slicer
The Brief
Create a fully autonomous waffle making machine with the ability to add whipped cream, maple syrup and chopped strawberries.
My subsystem had to slice a whole strawberry into five even pieces and dispense them onto a moving waffle, on cue from the wider robotic kitchen, with no hot glue or tape and every technique reused from the earlier units.
Mechanism
I was assigned to the strawberry section, working as the UI/Code guru and coordinating with the Create3 team to integrate via Airtable. Our design used 2 PVC pipes as channels for the strawberries, pushed by a rack and pinion stepper motor toward a knife. A Pi Camera detected a green colour stop signal ('skis' attached to strawberries). The knife used a second rack and pinion to cut the green top off, and a servo moved the strawberry to the slicing pipe where 5 cuts were made before dispensing onto the waffle below.
Stage 1: feed & de-stem
A rack-and-pinion stepper pushes the strawberry down a PVC channel until the camera flags its green marker at the knife line. A second rack then drives the knife down to remove the leafy top.
Stage 2: slice
A servo kicks the berry into the slicing channel, where the conveyor advances it in fixed CutThickness increments and drops the knife on each pass for five even slices.
Vision & Control
How two Raspberry Pis stayed in sync without a network.
The strawberry system ran two scripts working together. strawberry_cutter.py was the main robot brain — it polled Airtable to wait for a 'ready' signal from the wider cafe system, then ran two sequential phases: first_conveyor() homed the pusher stepper backward until a green GPIO trigger (fired by the camera Pi when it detected the strawberry's green ski marker) halted it at the knife, then drove the knife down until a blue GPIO trigger confirmed contact, used the servo to kick the strawberry into the second channel, and retracted. second_conveyor() then advanced the strawberry in precise CutThickness increments, dropping the knife on each pass for 5 even slices before retracting the whole conveyor. camera.py ran on a second Pi with a Pi Camera pointed at the strawberry channel — it continuously detected green (the ski marker on top of the strawberry) and blue (a coloured band indicating knife contact depth) using HSV colour masks and OpenCV contour detection. When the green centroid crossed a vertical trigger line it fired a HIGH signal on GPIO pin 40; when the blue centroid crossed a horizontal trigger line it fired GPIO pin 35. These direct GPIO-to-GPIO wires were the only connection between the two Pis, keeping the system fast and wireless-free.
Code
Main control script: polls Airtable for a ready signal, uses GPIO triggers from the camera Pi to home the pusher and detect the knife position, then drives three steppers and a servo to cut and slice the strawberry
import RPi.GPIO as GPIO
import time
import airtable_module as airtable
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BOARD)
# pins
Knife_PINS = [36, 40, 38, 37]
Pusher_PINS = [10, 7, 12, 8]
SECOND_CONVEYOR_PINS = [18, 15, 19, 16]
SERVO_PIN = 3
TRIGGER_PIN = 11
BLUE_TRIGGER_PIN = 31
SensorM = 35
GPIO.setup([SensorM], GPIO.IN)
GPIO.setup(TRIGGER_PIN, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.setup(BLUE_TRIGGER_PIN, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
STEP_SEQ = [
[1, 0, 0, 0],[1, 1, 0, 0],[0, 1, 0, 0],[0, 1, 1, 0],
[0, 0, 1, 0],[0, 0, 1, 1],[0, 0, 0, 1],[1, 0, 0, 1],
]
for pin in Knife_PINS + Pusher_PINS + SECOND_CONVEYOR_PINS:
GPIO.setup(pin, GPIO.OUT)
GPIO.output(pin, 0)
GPIO.setup(SERVO_PIN, GPIO.OUT)
pwm = GPIO.PWM(SERVO_PIN, 50)
pwm.start(0)
SERVO_DOWN = 90
SERVO_UP = 15
def SetAngle(angle):
duty = 2.5 + (angle / 180.0) * 10.0
pwm.ChangeDutyCycle(duty)
time.sleep(0.35)
pwm.ChangeDutyCycle(0)
STEP_DELAY_1 = 0.004
STEP_DELAY_2 = 0.02
STEP_DELAY_3 = 0.02
def stepper_run(pins, steps, delay, forward=True, label="stepper"):
seq = STEP_SEQ if forward else list(reversed(STEP_SEQ))
for i in range(steps):
pattern = seq[i % 8]
for pin, val in zip(pins, pattern):
GPIO.output(pin, val)
time.sleep(delay)
for pin in pins:
GPIO.output(pin, 0)
current_pos = 0
closed_pos = -1520
open_pos = 0
def move_knife_down_until_blue_trigger(max_steps=3200, chunk_steps=8):
global current_pos
moved = 0
while moved < max_steps:
if GPIO.input(BLUE_TRIGGER_PIN) == 1:
return True
stepper_run(Knife_PINS, chunk_steps, STEP_DELAY_1, forward=False, label="knife_down_search")
current_pos -= chunk_steps
moved += chunk_steps
return False
def first_conveyor():
global current_pos
SetAngle(SERVO_DOWN)
timeout = time.time() + 10
while True:
if time.time() > timeout:
raise RuntimeError("Timeout waiting for green trigger")
stepper_run(Pusher_PINS, 8, STEP_DELAY_2, forward=False, label="stepper")
if GPIO.input(TRIGGER_PIN) == 1:
break
stepper_run(Pusher_PINS, 40, STEP_DELAY_2, forward=True, label="stepper")
time.sleep(0.2)
if not move_knife_down_until_blue_trigger():
raise RuntimeError("Blue trigger not received")
time.sleep(0.5)
for _ in range(2):
SetAngle(SERVO_UP); time.sleep(0.5); SetAngle(SERVO_DOWN); time.sleep(1)
stepper_run(Knife_PINS, 1520, STEP_DELAY_1, forward=True, label="knife_up")
current_pos = open_pos
time.sleep(0.5)
for _ in range(2):
SetAngle(SERVO_UP); time.sleep(0.2); SetAngle(SERVO_DOWN); time.sleep(0.2)
CutThickness = 18
NumCuts = 5
StartPushSteps = 64
More = 5
Backwards = StartPushSteps + CutThickness * (NumCuts - 1) + CutThickness + More
def second_conveyor():
global current_pos
stepper_run(SECOND_CONVEYOR_PINS, StartPushSteps, STEP_DELAY_3, forward=True, label="stepper2")
for i in range(NumCuts - 1):
stepper_run(SECOND_CONVEYOR_PINS, CutThickness, STEP_DELAY_3, forward=True, label="stepper2")
time.sleep(1)
if not move_knife_down_until_blue_trigger():
raise RuntimeError(f"Blue trigger not received on cut {i + 1}")
time.sleep(0.5)
stepper_run(Knife_PINS, 1520, STEP_DELAY_1, forward=True, label="knife_up")
current_pos = open_pos
time.sleep(0.5)
stepper_run(SECOND_CONVEYOR_PINS, CutThickness + More, STEP_DELAY_3, forward=True, label="stepper2")
time.sleep(1)
stepper_run(SECOND_CONVEYOR_PINS, Backwards, STEP_DELAY_3, forward=False, label="stepper2")
try:
airtable.update_status("strawberry", "ready")
while True:
airtable.wait_until_ready("strawberry")
airtable.update_status("strawberry", "executing")
try:
first_conveyor()
second_conveyor()
airtable.update_status("strawberry", "success")
time.sleep(1)
except Exception as e:
print(f"Error: {e}")
airtable.update_status("strawberry", "failure")
time.sleep(1)
break
except KeyboardInterrupt:
print("\nKeyboard Interrupt")
finally:
pwm.stop()
for pin in Knife_PINS + Pusher_PINS + SECOND_CONVEYOR_PINS:
GPIO.output(pin, 0)
GPIO.cleanup()Runs on a second Pi: uses Pi Camera + OpenCV to detect green (strawberry ski marker) and blue (knife contact point) colours, firing GPIO output pins to signal the main cutter Pi in real time
import cv2
import numpy as np
import time
import RPi.GPIO as GPIO
from picamera2 import Picamera2
from libcamera import controls
OUTPUT_PIN = 40
BLUE_OUTPUT_PIN = 35
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BOARD)
GPIO.setup(OUTPUT_PIN, GPIO.OUT)
GPIO.setup(BLUE_OUTPUT_PIN, GPIO.OUT)
GPIO.output(OUTPUT_PIN, GPIO.LOW)
GPIO.output(BLUE_OUTPUT_PIN, GPIO.LOW)
picam2 = Picamera2()
config = picam2.create_preview_configuration(main={"size": (640, 480), "format": "RGB888"})
picam2.configure(config)
picam2.set_controls({"AfMode": controls.AfModeEnum.Continuous})
picam2.start()
time.sleep(0.5)
frame_width = 640
frame_height = 480
trigger_x = 542
trigger_y = 90
min_green_contour_area = 500
min_blue_contour_area = 500
lower_neon_green = np.array([60, 60, 100])
upper_neon_green = np.array([87, 255, 255])
lower_light_blue = np.array([85, 150, 150])
upper_light_blue = np.array([115, 255, 255])
last_green_pin_state = GPIO.LOW
last_blue_pin_state = GPIO.LOW
try:
while True:
frame = picam2.capture_array()
frame = cv2.flip(frame, -1)
blur = cv2.GaussianBlur(frame, (5, 5), 0)
hsv = cv2.cvtColor(blur, cv2.COLOR_BGR2HSV)
kernel = np.ones((5, 5), np.uint8)
green_mask = cv2.morphologyEx(cv2.inRange(hsv, lower_neon_green, upper_neon_green), cv2.MORPH_OPEN, kernel)
green_mask = cv2.morphologyEx(green_mask, cv2.MORPH_CLOSE, kernel)
blue_mask = cv2.morphologyEx(cv2.inRange(hsv, lower_light_blue, upper_light_blue), cv2.MORPH_OPEN, kernel)
blue_mask = cv2.morphologyEx(blue_mask, cv2.MORPH_CLOSE, kernel)
green_contours, _ = cv2.findContours(green_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
blue_contours, _ = cv2.findContours(blue_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
green_target_here = False
blue_target_here = False
if green_contours:
c = max(green_contours, key=cv2.contourArea)
if cv2.contourArea(c) > min_green_contour_area:
M = cv2.moments(c)
if M["m00"] != 0:
cx = int(M["m10"] / M["m00"])
if cx <= trigger_x:
green_target_here = True
if blue_contours:
c = max(blue_contours, key=cv2.contourArea)
if cv2.contourArea(c) > min_blue_contour_area:
M = cv2.moments(c)
if M["m00"] != 0:
cy = int(M["m01"] / M["m00"])
if cy >= trigger_y:
blue_target_here = True
GPIO.output(OUTPUT_PIN, GPIO.HIGH if green_target_here else GPIO.LOW)
GPIO.output(BLUE_OUTPUT_PIN, GPIO.HIGH if blue_target_here else GPIO.LOW)
cv2.imshow("Green Mask", green_mask)
cv2.imshow("Blue Mask", blue_mask)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
except KeyboardInterrupt:
pass
finally:
GPIO.output(OUTPUT_PIN, GPIO.LOW)
GPIO.output(BLUE_OUTPUT_PIN, GPIO.LOW)
GPIO.cleanup()
cv2.destroyAllWindows()Live Café Demo
Outcome
The overall cafe was a huge success and our strawberry cutting machine worked great. We had slight issues running with more than 3 strawberries consecutively but it was an amazing outcome. The work put in during the last couple days was vital to overcome tolerance issues.
If I built it again
- Develop a conveyor belt system for more reliability over time
- Improve accuracy of the 2nd chopper for more even slices
Tools
The other five units
Gripper, colour sorter, two line followers, and vision navigation, all with code.