Back to Projects
Autonomous strawberry slicer
Robotics / Mechatronics

Autonomous Strawberry Slicer

April 2026 Strawberry subsystem: mechanical & software lead

The Brief

Create a fully autonomous waffle making machine with the ability to add whipped cream, maple syrup and chopped strawberries.

My part had to cut a whole strawberry into five even slices and drop them onto a passing waffle when the rest of the kitchen said go. Same rules as every unit: no hot glue, no tape, and reuse what we'd already learnt.

No hot glue or tapeMust use all of the skills learnt in previous projects

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 stepper on a rack and pinion pushes the strawberry down a PVC channel. When the camera sees the green marker reach the knife line, a second rack drives the knife down and takes the top off.

Stage 2: slice

A servo knocks the berry into the slicing channel. A conveyor moves it forward a fixed step at a time and the knife drops on each one, five times.

Vision & Control

Two Raspberry Pis, no network between them.

Two scripts. strawberry_cutter.py ran the machine. It waited on Airtable for a ready signal from the rest of the café, then ran two phases. first_conveyor() pushed the strawberry until the camera Pi flagged the green marker on a GPIO pin, drove the knife down until a second pin confirmed contact, kicked the berry into the second channel with the servo, and reset. second_conveyor() moved the berry forward one slice thickness at a time and dropped the knife on each, five times, then wound everything back. camera.py ran on the other Pi with the camera pointed at the channel. It masked green and blue in HSV and found the contours with OpenCV. Green crossing a vertical line set pin 40 high, blue crossing a horizontal line set pin 35. Those two wires were the whole link between the Pis. No WiFi, nothing to drop out.

Code

strawberry_cutter.py

Main script. Waits on Airtable, uses GPIO triggers from the camera Pi to home the pusher and find the knife, then runs three steppers and a servo.

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()
camera.py

Runs on the second Pi. Picks out green (strawberry marker) and blue (knife contact) with the Pi Camera and OpenCV, and flips GPIO pins to tell the cutter Pi.

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

PythonRaspberry PiOpenCVPi CameraAirtable APIRack & PinionNEMA SteppersPVC Fabrication

The other five units

Gripper, colour sorter, two line followers, and vision navigation, all with code.

ME35 Robotics Portfolio