Back to Projects
Autonomous strawberry slicer
Robotics / Mechatronics

Autonomous Strawberry Slicer

April 2026 Strawberry subsystem: mechanical & software lead
5
Slices / Strawberry
2
Raspberry Pis
3+1
Steppers + Servo

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.

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

strawberry_cutter.py

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

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

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