Lego Racer

I got the inspiration for this project when I saw Mario Kart Live: Home Circuit. With your Nintendo Switch, you control a real life race cart across tracks in your living room.

I’ve used my Lego Mindstorms to build a similar setup:

.

.

The details

Initialization

During initialization, the wheels are turned left and right until there’s resistance. That way, it is possible to position them exactly in the center.

.

.

Playstation controller

The racer is controlled via a PS4 controller. I’ve used this YouTube tutorial to learn how to do this. Honestly, this was much easier than I thought it would be.

Smartphone holder

The racecar has a place to support a smartphone. I used it to make videos while driving. But it’s also possible to pair my phone with my TV, and then I can play on my TV with a view just like a real Mario Kart game. That makes racing a lot more difficult due to the limited view.

Collisions

I used the Mindstorms touch sensor to detect frontal collisions. If you bump into something, your speed is greatly reduced.

Laps

The racer automatically detects laps and checkpoints with the Mindstorms color sensor. Red means a new lap, and blue is a checkpoint. You can create a circuit by positioning red and blue sheets of paper across your house.

On the EV3 hub, you can consult the lap times. This way, you can try to beat your own record.

Programming

I’ve used MicroPython for programming the EV3. Programming was a bit harder than normal, because I had to support 3 events simultaneously: controller inputs, collision detection and lap detection.

I used different threads for these events. This guide was helpful in setting up multithreading in MicroPython.

#!/usr/bin/env pybricks-micropython
from pybricks.hubs import EV3Brick
from pybricks.ev3devices import (Motor, TouchSensor, ColorSensor,
                                 InfraredSensor, UltrasonicSensor, GyroSensor)
from pybricks.parameters import Port, Stop, Direction, Button, Color
from pybricks.tools import wait, StopWatch, DataLog
from pybricks.robotics import DriveBase
from pybricks.media.ev3dev import SoundFile, ImageFile
from threading import Thread
import sys
from time import sleep

import struct


# This program requires LEGO EV3 MicroPython v2.0 or higher.
# Click "Open user guide" on the EV3 extension tab for more information.

# Variables
speed_stop_threshold = 15
rotation_threshold = 15
rotation_limiter = 100 #limit the max rotation by a number of degrees
steering_speed  = 800
full_speed       = 1.0 # in %
limited_speed   = 0.5 # in %
reverse_speed   = 0.6 # in %
speed_scaler    = full_speed
slowdown_time   = 8 # s
color_time   = 5 # s
checkpoint_1 = Color.RED
checkpoint_2 = Color.BLUE
lap_counter = 0


# Create your objects here.
ev3 = EV3Brick()
ev3.speaker.set_volume(100,'_all_')
ev3.speaker.set_speech_options('en','f3',None,None)
drive_motor = Motor(Port.B)
steering_motor = Motor(Port.C) 
touch_sensor = TouchSensor(Port.S3)
color_sensor = ColorSensor(Port.S2)
timer_lap1 = StopWatch()
timer_lap1.pause()
timer_lap1.reset()
timer_lap2 = StopWatch()
timer_lap2.pause()
timer_lap2.reset()
timer_lap3 = StopWatch()
timer_lap3.pause()
timer_lap3.reset()




# Initialize steering motor
ev3.speaker.say('Ready')
ev3.light.on(Color.RED)

steering_motor.run_until_stalled(180,then=Stop.HOLD,duty_limit=40)
steer_left = steering_motor.angle() - rotation_limiter

steering_motor.run_until_stalled(-180,then=Stop.HOLD,duty_limit=40)
steer_right = steering_motor.angle() + rotation_limiter

ev3.speaker.say('Set')
ev3.light.on(Color.YELLOW)

steer_center = (steer_left + steer_right) /2
steering_motor.run_target(steering_speed,steer_center,then=Stop.HOLD,wait=True)

#print(str(steer_left))
#print(str(steer_right))
#print(str(steer_center))


# Start program.
ev3.speaker.say('Go')
ev3.light.on(Color.GREEN)


# Driving forward or backward + stop without braking if speed is below threshold
#speed in % between -100 and 100
def drive(speed):
    if speed > speed_stop_threshold or speed < speed_stop_threshold * -1:
        #print(str(speed*speed_scaler)+"                   speed: "+str(speed)+"     speed scaler: "+str(speed_scaler))
        if speed > 0:
            drive_motor.dc(speed * speed_scaler)
        else:
            drive_motor.dc(speed * speed_scaler * reverse_speed)
    else:
        drive_motor.stop()

# Steering left or right + center if rotation is below threshold
#rotation in % between -100 (left) and 100 (right)
def steer(rotation):
    if rotation > rotation_threshold or rotation < rotation_threshold * -1:
        steering_motor.run_target(steering_speed,scale(rotation,(-100,100),(steer_right,steer_left)),then=Stop.HOLD,wait=False)
    else:
        steering_motor.run_target(steering_speed,steer_center,then=Stop.HOLD,wait=False)

# Reduce the speed
def slow_down():
    global speed_scaler
    speed_scaler = limited_speed
    #print("Slow")

# Reset the speed
def reset_speed():
    global speed_scaler
    speed_scaler = full_speed
    #print("Fast")

       

# Scale a value
def scale(val, src, dst):
    return int((val - src[0]) / (src[1] - src[0]) * (dst[1] - dst[0]) + dst[0])

#Scale the stick to a percent -100% to 100%
def scale_stick_to_percent(value):
    return scale(value, (0,255), (100, -100))

#Check for collisions, and slow down if a collision is detected
def collision_check():
    while True:
        if touch_sensor.pressed():
            slow_down()
            sleep(slowdown_time)
            reset_speed()

#Start a separate thread for collision checks
collision_thread = Thread(target=collision_check)
collision_thread.start()

#keep track of laps & timers
def lap_increase():
    global timer_lap1
    global timer_lap2
    global timer_lap3
    global lap_counter

    lap_counter = lap_counter+1

    if lap_counter == 1:
        timer_lap1.resume()
        ev3.screen.clear()
        ev3.screen.draw_text(0,0,'LAP 1: '+convert_ms(timer_lap1.time()),Color.BLACK,None)
        ev3.screen.draw_text(0,20,'LAP 2: '+convert_ms(timer_lap2.time()),Color.BLACK,None)
        ev3.screen.draw_text(0,40,'LAP 3: '+convert_ms(timer_lap3.time()),Color.BLACK,None)
        ev3.speaker.say('Lap 1')

    if lap_counter == 2:
        timer_lap1.pause()
        ev3.screen.clear()
        ev3.screen.draw_text(0,0,'LAP 1: '+convert_ms(timer_lap1.time()),Color.BLACK,None)
        ev3.screen.draw_text(0,20,'LAP 2: '+convert_ms(timer_lap2.time()),Color.BLACK,None)
        ev3.screen.draw_text(0,40,'LAP 3: '+convert_ms(timer_lap3.time()),Color.BLACK,None)
        timer_lap2.resume()
        ev3.speaker.say('Lap 2')

    if lap_counter == 3:
        timer_lap2.pause()
        ev3.screen.clear()
        ev3.screen.draw_text(0,0,'LAP 1: '+convert_ms(timer_lap1.time()),Color.BLACK,None)
        ev3.screen.draw_text(0,20,'LAP 2: '+convert_ms(timer_lap2.time()),Color.BLACK,None)
        ev3.screen.draw_text(0,40,'LAP 3: '+convert_ms(timer_lap3.time()),Color.BLACK,None)
        timer_lap3.resume()
        ev3.speaker.say('Lap 3')

    if lap_counter > 3:
        timer_lap3.pause()
        ev3.screen.clear()
        ev3.screen.draw_text(0,0,'LAP 1: '+convert_ms(timer_lap1.time()),Color.BLACK,None)
        ev3.screen.draw_text(0,20,'LAP 2: '+convert_ms(timer_lap2.time()),Color.BLACK,None)
        ev3.screen.draw_text(0,40,'LAP 3: '+convert_ms(timer_lap3.time()),Color.BLACK,None)
        ev3.speaker.say('Finish')



#Check the color sensor, and take action if a color is detected
def color_check():
    while True:
        while True:
            color = color_sensor.color()
            if color == checkpoint_1:
                break
        
        print('checkpoint 1 detected')
        lap_increase()

        while True:
            color = color_sensor.color()
            if color == checkpoint_2:
                break
        
        print('checkpoint 2 detected')
        ev3.speaker.say('Checkpoint passed')


#Start a separate thread for color checks
color_thread = Thread(target=color_check)
color_thread.start()

def convert_ms(ms):
     seconds=(ms/1000)%60
     minutes=(ms/(1000*60))%60
     return str(int(minutes))+':'+str(int(seconds))

#print the lap timer
def lap_timer():
    global timer_lap1
    global timer_lap2
    global timer_lap3

    ev3.screen.clear()

    while True:
        ev3.screen.clear()
        ev3.screen.draw_text(0,0,'LAP 1: '+convert_ms(timer_lap1.time()),Color.BLACK,None)
        ev3.screen.draw_text(0,20,'LAP 2: '+convert_ms(timer_lap2.time()),Color.BLACK,None)
        ev3.screen.draw_text(0,40,'LAP 3: '+convert_ms(timer_lap3.time()),Color.BLACK,None)

        sleep(5)




def input_handler():
    # Open the Gamepad event file:
    # /dev/input/event3 is for PS3 gamepad
    # /dev/input/event4 is for PS4 gamepad
    # look at contents of /proc/bus/input/devices if either one of them doesn't work.
    # use 'cat /proc/bus/input/devices' and look for the event file.
    infile_path = "/dev/input/event4"

    # open file in binary mode
    in_file = open(infile_path, "rb")

    # Read from the file
    # long int, long int, unsigned short, unsigned short, unsigned int
    FORMAT = 'llHHI'    
    EVENT_SIZE = struct.calcsize(FORMAT)
    event = in_file.read(EVENT_SIZE)

    while event:
        (tv_sec, tv_usec, event_type, event_code, event_value) = struct.unpack(FORMAT, event)

        # Determine left analog stick Y-axis movements
        if event_type == 3 and event_code == 1:  
            drive(scale_stick_to_percent(event_value))

        # Determine right analog stick X-axis movements
        if event_type == 3 and event_code == 3:  
            steer(scale_stick_to_percent(event_value))

        # Finally, read another event
        event = in_file.read(EVENT_SIZE)

    in_file.close()

#Start a separate thread for the input handler
input_thread = Thread(target=input_handler())
input_thread.start()
print('input start')

Other projects

Check out some of my other Lego Mindstorms EV3 projects:

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s