Train speed control

Python code and building instructions for the LEGO City Cargo Train (60198).

Train speed control

Make a train drive at a constant speed, independent of load and battery level.

Design modifications

Build any train using the standard instructions. Mount the color distance sensor in the frame, as shown in the video. Connect the motor to port A and connect the sensor to port B.


This program makes the train drive at a constant speed. It works by counting the tracks using the Color and Distance Sensor. The motor “power” is automatically increased if the counted position is below the target position, and reduced if it is too far ahead. The target position steadily increases over time. This makes the train follow the target at the same speed.

In this video, there’s a white surface underneath the tracks. If your background is different, you can change the reflection values accordingly, as shown in the comments below. You could also adapt the script so it responds to different colors instead of reflected light intensity.

from pybricks.pupdevices import DCMotor, ColorDistanceSensor
from import StopWatch
from pybricks.parameters import Port

# Initialize the motor and the sensor.
motor = DCMotor(Port.A)
sensor = ColorDistanceSensor(Port.B)

# These are the sensor reflection values in this setup.
# Adapt them to match your ambient light conditions.
LIGHT = 57
DARK = 16

# Threshold values. We add a bit of hysteresis to make
# sure we skip extra changes on the edge of each track.
hysteresis = (LIGHT - DARK) / 4
threshold_up = (LIGHT + DARK) / 2 + hysteresis
threshold_down = (LIGHT + DARK) / 2 - hysteresis

# Initial position state.
on_track = True
position = 0

# Desired drive speed in mm per second.
SPEED = 300

# It's two studs (16 mm) for each position increase.

# Start a timer.
watch = StopWatch()

while True:

    # Measure the reflection.
    reflection = sensor.reflection()

    # If the reflection exceeds the threshold, increment position.
    if (reflection > threshold_up and on_track) or (reflection < threshold_down and not on_track):
        on_track = not on_track
        position += 1
    # Compute the target position based on the time.
    target_count = watch.time() / 1000 * SPEED / MM_PER_COUNT

    # The duty cycle is the position error times a constant gain.
    duty = 2*(target_count - position)

    # Apply the duty cycle.

Building instructions

This project was submitted by The Pybricks Team.