I designed the robot using Fusion360 and fabricated it mostly through 3D printing with a little bit of laser cutting acrylic.
My partner Kilder wrote the majority of the code, but here's what it looks like:
I dont want to look at the code anymore!!
from machine import Pin, PWM, I2C
import time
# --------------------------
# Onboard button
# --------------------------
button = Pin(34, Pin.IN)
print("Waiting for button (D34) press...")
while button.value() == 1:
time.sleep(0.05)
print("Button pressed! Starting line following...")
time.sleep(0.2)
# --------------------------
# I2C color sensors
# --------------------------
i2c_left = I2C(0, scl=Pin(22), sda=Pin(21), freq=400000)
i2c_right = I2C(1, scl=Pin(25), sda=Pin(26), freq=400000)
VEML_ADDR = 0x10
REG_CONF = 0x00
REG_RED = 0x08
REG_GREEN = 0x09
REG_BLUE = 0x0A
REG_WHITE = 0x0B
def veml_init(i2c):
i2c.writeto_mem(VEML_ADDR, REG_CONF, bytes([0x00]))
time.sleep(0.1)
def veml_read(i2c):
def read16(reg):
data = i2c.readfrom_mem(VEML_ADDR, reg, 2)
return int.from_bytes(data, 'little')
return read16(REG_RED), read16(REG_GREEN), read16(REG_BLUE), read16(REG_WHITE)
veml_init(i2c_left)
veml_init(i2c_right)
# --------------------------
# Encoder class
# --------------------------
class Count:
def __init__(self, A_pin, B_pin):
self.A = Pin(A_pin, Pin.IN)
self.B = Pin(B_pin, Pin.IN)
self.counter = 0
self.A.irq(self.cb, Pin.IRQ_RISING | Pin.IRQ_FALLING)
self.B.irq(self.cb, Pin.IRQ_RISING | Pin.IRQ_FALLING)
def cb(self, pin):
other, inc = (self.B, 1) if pin == self.A else (self.A, -1)
self.counter += -inc if pin.value() != other.value() else inc
def value(self):
return self.counter
# --------------------------
# Motor class
# --------------------------
class Motor:
def __init__(self, pwm_pin, dir_pin, encA, encB):
self.pwm = PWM(Pin(pwm_pin))
self.pwm.freq(1000)
self.dir = Pin(dir_pin, Pin.OUT)
self.encoder = Count(encA, encB)
self.stop()
def stop(self):
self.pwm.duty_u16(0)
self.dir.value(0)
def set_speed(self, direction=1, speed_percent=50):
self.dir.value(direction)
self.pwm.duty_u16(int(speed_percent / 100 * 65535))
def position(self):
return self.encoder.value()
# --------------------------
# Initialize motors
# --------------------------
left_motor = Motor(pwm_pin=13, dir_pin=12, encA=32, encB=39)
right_motor = Motor(pwm_pin=14, dir_pin=27, encA=33, encB=36)
# --------------------------
# Color detection & smoothing
# --------------------------
colors_left = {'black': (90,91,53,222), 'red': (201,124,77,393),
'green': (168,213,86,369), 'blue': (150,220,198,399),
'white': (421,518,339,852)}
colors_right = {'black': (92,91,51,229), 'red': (206,128,78,403),
'green': (167,209,84,374), 'blue': (154,217,185,406),
'white': (423,514,316,862)}
def make_ranges(refs, tol=40):
ranges = {}
for c,v in refs.items():
ranges[c] = ((v[0]-tol,v[0]+tol),(v[1]-tol,v[1]+tol),(v[2]-tol,v[2]+tol),(v[3]-tol*2,v[3]+tol*2))
return ranges
ranges_left = make_ranges(colors_left)
ranges_right = make_ranges(colors_right)
def classify_color(r,g,b,w,ranges):
for color,(rr,gg,bb,ww) in ranges.items():
if rr[0]<=r<=rr[1] and gg[0]<=g<=gg[1] and bb[0]<=b<=bb[1] and ww[0]<=w<=ww[1]:
return color
return "Unknown"
window_size = 5
left_history = []
right_history = []
def add_history(history,value):
history.append(value)
if len(history) > window_size:
history.pop(0)
def majority_vote(history):
counts = {}
for item in history:
counts[item] = counts.get(item,0)+1
max_count = 0
winner = "Unknown"
for k,v in counts.items():
if v>max_count:
max_count=v
winner=k
return winner
# --------------------------
# Main loop: shorter bursts + rest
# --------------------------
BASE_SPEED = 10
TURN_SCALE = 0.3
RUN_DURATION = 0.5 # seconds per motor burst
REST_DURATION = 0.5 # seconds rest between bursts
print("=== LINE FOLLOWING MODE ===")
try:
while True:
# Read color sensors
lr,lg,lb,lw = veml_read(i2c_left)
rr,rg,rb,rw = veml_read(i2c_right)
add_history(left_history, classify_color(lr,lg,lb,lw, ranges_left))
add_history(right_history, classify_color(rr,rg,rb,rw, ranges_right))
sm_left = majority_vote(left_history)
sm_right = majority_vote(right_history)
# Decide motor speeds: black = full, white = slower (turn)
left_speed = BASE_SPEED if sm_left=='black' else BASE_SPEED*TURN_SCALE
right_speed = BASE_SPEED if sm_right=='black' else BASE_SPEED*TURN_SCALE
# Run motors simultaneously
left_motor.set_speed(direction=1, speed_percent=left_speed)
right_motor.set_speed(direction=1, speed_percent=right_speed)
# Burst duration
time.sleep(RUN_DURATION)
# Stop motors
left_motor.stop()
right_motor.stop()
# Rest period
time.sleep(REST_DURATION)
# Debug
print(f"Left: {sm_left} | Right: {sm_right} | Encoders: L={left_motor.position()} R={right_motor.position()}")
except KeyboardInterrupt:
print("\nStopping motors...")
finally:
left_motor.stop()
right_motor.stop()
left_motor.pwm.deinit()
right_motor.pwm.deinit()
print("Motors stopped ✅")