SteerPy is an autonomous driving playground that runs in the browser. Open the page, write Python, press run, watch the car react. That is the entire setup.

The problem

When I started learning robotics, the hardest part was not the theory. It was getting an environment up and running before actually try something. By the time ROS was installed, the Docker container was configured, and the simulator was compiling, I had forgotten what I wanted to learn.

I believe the best way to learn is to get your hands dirty. But that only works if the friction between “I want to try this” and “I am trying it” is close to zero.

This series uses SteerPy as a playground. Each post picks one concept, tells you exactly which file to change, and lets you see the result immediately. No setup required. Just you, a browser, and a car waiting to be broken.

Takeaway: The environment should never be the reason you did not learn something.

Why it matters

Most robotics simulators are built for researchers who already know what they are doing. SteerPy is built for people who are still figuring it out. The physics is configurable but sane by default. The interface is a code editor and a running car. The feedback is immediate.

You can run it two ways:

For this series, the browser version is all you need.

Takeaway: SteerPy removes every barrier between you and the running code.

How it works

SteerPy gives you two Python files to edit. Everything else runs automatically.

controller.py is called every simulation step. It receives the car’s current state and the trajectory from the planner, and returns [throttle, steer]. Throttle is 0 to 1, steer is -1 (full left) to +1 (full right).

# controller.py
# controller(car, trajectory) -> [throttle, steer]
#
# Available state:
#   car.x, car.y, car.angle, car.speed
#   car.length, car.width, car.wheelbase
#   car.min_speed, car.max_speed
#   car.steer_angle, car.vx, car.vy

def controller(car, trajectory):
    if not trajectory:
        return [0.0, 0.0]

    throttle_cmd = 0.0
    steer_cmd = 0.0
    return [throttle_cmd, steer_cmd]

Right now this does nothing. The car sits still. That is your starting point.

planner.py decides where the car should go. It receives the car’s state and a world model containing road waypoints, obstacle positions, and sensor readings. It returns a list of (x, y, target_speed) points for the controller to track.

# planner.py
# planner(car, world_model) -> [(x, y, target_speed), ...]
#
# World model:
#   world_model.road_data.waypoints  # road centerline points
#   world_model.road_data.width      # lane width in meters
#   world_model.obstacles            # list of {x, y, length, width, heading_deg}
#   world_model.loop                 # True if the track loops
#
# Sensors (meters to nearest obstacle, -1 = clear):
#   car.sensors.front, car.sensors.rear
#   car.sensors.left, car.sensors.right
#   car.sensors.lidar  # N-beam list, index 0 = front, increasing CCW

def planner(car, world_model=None):
    return []

No trajectory returned means the controller has nothing to aim at. The car stops. Both files start empty on purpose: every concept in this series is you filling them in from scratch.

The simulation loop is: planner picks waypoints, controller steers toward them, car moves, repeat. Hit Ctrl+S to apply your changes without restarting.

SteerPy also supports multiple physics models (kinematic, bicycle, Ackermann, drift) and configurable sensors including six-direction distance sensors and LiDAR. You will not need any of that yet, but it is there when a post calls for it.

Takeaway: Two empty functions is all it takes to start. The rest of this series is filling them in, one concept at a time.

Hello world

Before diving into any theory, try this. Swith to controller.py tab and set a fixed throttle and steer:

def controller(car, trajectory):
    throttle_cmd = 0.5   # half throttle
    steer_cmd = 0.2      # slight right
    return [throttle_cmd, steer_cmd]

Hit Ctrl+S. The car moves, drifts right, and eventually leaves the road. That is expected. You just confirmed the controls work.

Now swith planner.py tab and return a hardcoded straight-line trajectory ahead of the car:

def planner(car, world_model=None):
    # Build a simple straight-line path 5 meters ahead, 10 points
    import math
    points = []
    for i in range(1, 11):
        x = car.x + i * math.cos(math.radians(car.angle))
        y = car.y + i * math.sin(math.radians(car.angle))
        speed = 3.0  # target speed in m/s
        points.append((x, y, speed))
    return points

The controller now has a trajectory to follow. It still ignores it (both commands are hardcoded), but you can see the waypoints appear on screen. In the next post you will wire the two together so the controller actually steers toward them.

Takeaway: Break it in the simplest possible way first. It tells you more than reading the docs.

← All Posts