Hana Sestakova
BallPal
I led firmware architecture and systems integration for an autonomous robotic collector built on Raspberry Pi and Python. I wrote 100% of the motor control and navigation codebase, coordinating with a mechanical engineer on hardware integration. The project achieved 2nd place in the NYU Tandon Nick Russo Award, with a high object retrieval success rate during trials.



Showcase Poster
AutoCAD Chassis
Code Flowchart

Code for Raspberry Pi 5

​
# Import necessary libraries
import cv2
import numpy as np
import lgpio
from time import time, sleep
Â
# Initialize GPIO chip
h = lgpio.gpiochip_open(0)
Â
# Define motor GPIO pins
IN1, IN2 = 22, 20Â Â
IN3, IN4 = 12, 16Â Â
motor_pins = [IN1, IN2, IN3, IN4]
Â
# Define ultrasonic sensor pins
TRIG, ECHO = 4, 17Â Â
Â
# Configure motor pins as outputs
for pin in motor_pins:
    lgpio.gpio_claim_output(h, pin)
Â
# Configure ultrasonic sensor pins
lgpio.gpio_claim_output(h, TRIG)
lgpio.gpio_claim_input(h, ECHO)
Â
# Initialize camera
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 320)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 240)
Â
# Define HSV color range for detecting orange
lower_orange = np.array([9, 130, 130])
upper_orange = np.array([22, 255, 255])
Â
# Basic movement control functions for robot
def motor_forward(): #moves forward
    lgpio.gpio_write(h, IN1, 1)
    lgpio.gpio_write(h, IN2, 0)
    lgpio.gpio_write(h, IN3, 1)
    lgpio.gpio_write(h, IN4, 0)
Â
def motor_left(): #moves left
    lgpio.gpio_write(h, IN1, 0)
    lgpio.gpio_write(h, IN2, 1)
    lgpio.gpio_write(h, IN3, 1)
    lgpio.gpio_write(h, IN4, 0)
Â
def motor_right(): #moves right
    lgpio.gpio_write(h, IN1, 1)
    lgpio.gpio_write(h, IN2, 0)
    lgpio.gpio_write(h, IN3, 0)
    lgpio.gpio_write(h, IN4, 1)
Â
def motor_reverse(): #moves backward
    lgpio.gpio_write(h, IN1, 0)
    lgpio.gpio_write(h, IN2, 1)
    lgpio.gpio_write(h, IN3, 0)
    lgpio.gpio_write(h, IN4, 1)
Â
def motor_stop(): #stops moving
    for pin in motor_pins:
        lgpio.gpio_write(h, pin, 0)
Â
# Brush VEX motor control setup
vex_IN3 = 19
vex_IN4 = 26
ENB = 18 #This pin is for power control of the motor
Â
# Configure Brush VEX motor pins
lgpio.gpio_claim_output(h, vex_IN3)
lgpio.gpio_claim_output(h, vex_IN4)
lgpio.gpio_claim_output(h, ENB)
Â
# Function to move brush VEX motor forward at given speed (0–100%)
def vex_motor_forward(speed_percent):
    lgpio.gpio_write(h, vex_IN3, 1)
    lgpio.gpio_write(h, vex_IN4, 0)
    lgpio.tx_pwm(h, ENB, 10000, speed_percent)
Â
# Stop brush VEX motor
def vex_motor_stop():
    print("Stopping motor")
    lgpio.gpio_write(h, vex_IN3, 0)
    lgpio.gpio_write(h, vex_IN4, 0)
Â
# Read distance from ultrasonic sensor
def get_distance():
    lgpio.gpio_write(h, TRIG, 1)
    sleep(0.00001)
    lgpio.gpio_write(h, TRIG, 0)
    pulse_start = time()
    while lgpio.gpio_read(h, ECHO) == 0:
        pulse_start = time()
    pulse_end = time()
    while lgpio.gpio_read(h, ECHO) == 1:
        pulse_end = time()
    duration = pulse_end - pulse_start
    distance = round(duration * 17150, 2)
    return distance
Â
# Core behavior loop: zigzag, track ball, avoid obstacles
def move_zigzag():
    zigzag_state = 0 # Keeps track of zigzag direction
    while True:
        distance = get_distance()
        print(f"Distance: {distance} cm")
        # Read camera frame
        ret, frame = cap.read()
        if not ret:
            continue
        # Apply HSV mask for orange ball detection
        hsv_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
        mask = cv2.inRange(hsv_frame, lower_orange, upper_orange)
        contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        # Check if ball is detected
        ball_detected = False
        ball_x = None
        for cnt in contours:
            area = cv2.contourArea(cnt)
            if area > 300:
                ball_detected = True
                x, y, w, h = cv2.boundingRect(cnt)
                ball_x = x + w // 2
                break
        if ball_detected: #when ball is detected…
            print("Ball Detected - Tracking Faster!")
            frame_center = frame.shape[1] // 2
            reaction_time = 0.2
            # Turn left toward ball
            if ball_x < frame_center - 60:
                print("Adjusting Left for .2 s")
                motor_left()
                start_time = time()
                while time() - start_time < reaction_time: #this loop makes camera constantly update
Â
                    ret, frame = cap.read()
                    if ret:
                        hsv_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
                        mask = cv2.inRange(hsv_frame, lower_orange, upper_orange)
                        cv2.imshow("Camera Feed", frame)
                        cv2.imshow("Orange Mask", mask)
                        if cv2.waitKey(1) & 0xFF == ord('q'):
                            return
Â
            # Turn right toward ball
            elif ball_x > frame_center + 60:
                print("Adjusting Right for .2 s")
                motor_right()
                start_time = time()
                while time() - start_time < reaction_time: #this loop makes camera constantly update
Â
                    ret, frame = cap.read()
                    if ret:
                        hsv_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
                        mask = cv2.inRange(hsv_frame, lower_orange, upper_orange)
                        cv2.imshow("Camera Feed", frame)
                        cv2.imshow("Orange Mask", mask)
                        if cv2.waitKey(1) & 0xFF == ord('q'):
                            return
Â
            # Move forward toward ball
            else:
                print("Moving Forward Toward Ball for .2 s")
                motor_forward()
                start_time = time()
                while time() - start_time < 0.7: #this loop makes the camera constantly update
                    ret, frame = cap.read()
                    if ret:
                        hsv_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
                        mask = cv2.inRange(hsv_frame, lower_orange, upper_orange)
                        cv2.imshow("Camera Feed", frame)
                        cv2.imshow("Orange Mask", mask)
                        if cv2.waitKey(1) & 0xFF == ord('q'):
                            return
                vex_motor_forward(70)
                sleep(1.25)
                vex_motor_forward(98)
                sleep(3)
Â
            # Stop after interaction
            vex_motor_stop()
            motor_stop()
            start_time = time()
            while time() - start_time < 1.5:
                ret, frame = cap.read()
                if ret:
                    hsv_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
                    mask = cv2.inRange(hsv_frame, lower_orange, upper_orange)
                    cv2.imshow("Camera Feed", frame)
                    cv2.imshow("Orange Mask", mask)
                    if cv2.waitKey(1) & 0xFF == ord('q'):
                        return
Â
        # Obstacle avoidance
        elif distance < 75:
            print("Wall detected! Avoiding")
            motor_reverse()
            sleep(0.8)
            motor_stop()
            motor_right()
            sleep(0.6)
            motor_stop()
            start_time = time()
            while time() - start_time < 1.5: #this loop makes the camera constantly update
Â
                distance = get_distance()
                ret, frame = cap.read()
                if ret:
                    cv2.imshow("Camera Feed", frame)
                    cv2.imshow("Orange Mask", mask)
                    if cv2.waitKey(1) & 0xFF == ord('q'):
                        return
Â
        # Default zigzag movement
        else:
            print("Zigzagging...")
            if zigzag_state == 0:
                motor_left()
                start_time = time()
                while time() - start_time < 0.17: #this loop makes the camera constantly update
                    distance = get_distance()
                    ret, frame = cap.read()
                    if ret:
                        cv2.imshow("Camera Feed", frame)
                        cv2.imshow("Orange Mask", mask)
                        if cv2.waitKey(1) & 0xFF == ord('q'):
                            return
                motor_forward()
                zigzag_state = 1
            else:
                motor_right()
                start_time = time()
                while time() - start_time < 0.17:
                    distance = get_distance()
                    ret, frame = cap.read()
                    if ret:
                        cv2.imshow("Camera Feed", frame)
                        cv2.imshow("Orange Mask", mask)
                        if cv2.waitKey(1) & 0xFF == ord('q'):
                            return
                motor_forward()
                zigzag_state = 0
Â
            start_time = time()
            while time() - start_time < 0.3: #this loop makes the camera constantly update
                distance = get_distance()
                ret, frame = cap.read()
                if ret:
                    cv2.imshow("Camera Feed", frame)
                    cv2.imshow("Orange Mask", mask)
                    if cv2.waitKey(1) & 0xFF == ord('q'):
                        return
Â
            motor_stop()
            start_time = time()
            while time() - start_time < 1.5:
                distance = get_distance()
                ret, frame = cap.read()
                if ret:
                    cv2.imshow("Camera Feed", frame)
                    cv2.imshow("Orange Mask", mask)
                    if cv2.waitKey(1) & 0xFF == ord('q'):
                        return
Â
# Start behavior loop
try:
    move_zigzag()
except KeyboardInterrupt: #makes the typical error exceptionÂ
    print("Stopping")
    motor_stop()
    lgpio.gpiochip_close(h)
    cap.release()
    cv2.destroyAllWindows()