In this project, we will show you how to control the LEGO® Technic Cat D11 Bulldozer (42131) with the Xbox Controller.

This is a fun way to drive the bulldozer around without having to look at your phone screen all time time. You’ll also use the controller to switch between the functions of the blade, ripper, and ladder.

Driving the LEGO® Technic Cat D11 Bulldozer (42131) with the Xbox Controller.
Driving the LEGO® Technic Cat D11 Bulldozer (42131) with the Xbox Controller.

Requirements

To follow this project, you will need the following:

See this overview for all compatible models.

Understanding the code

The program consists of two main tasks that run in parallel. The first task handles the driving and steering of the bulldozer, as well as powering the function motor.

The second task controls the position of the motor that selects which function is being operated. The details are included as comments in the program below.

When the program begins, it resets the function selector to the zero position, corresponding to the blade up and down movement. You can select any function by pressing the A/B/X/Y buttons. This will switch the function selector as follows:

  • X (blue): Moves the blade up and down.
  • B (red): Tilts the blade back and forth.
  • Y (yellow): Moves the ladder up and down.
  • A (green): Moves the ripper up and down.

Once the function is selected, use the bumper buttons to control the motor that powers the selected function.

Use the direction pad to control the tracks. You can change the drive acceleration in the main program to a lower value to make the vehicle start and stop driving more gradually. You can also change the speed values corresponding the left and right motor blocks.

This program lets you drive the LEGO® Technic Cat D11 Bulldozer (42131)
  with the Xbox Controller.
This program lets you drive the LEGO® Technic Cat D11 Bulldozer (42131) with the Xbox Controller.

Running the Pybricks program

This project uses Pybricks on your LEGO hub. Pybricks makes your creations come alive and helps you unlock the full potential of your LEGO Technic, City, MINDSTORMS, BOOST, or Spike sets.

If you haven’t already, install Pybricks on your hub as shown below, or check out our getting started guide for more details. You can go back to the LEGO firmware and apps at any time.

Install the Pybricks firmware.
settings
install

Now import the program you downloaded earlier, as shown below. Click to connect your hub and ▶ to start!

Import a Pybricks Code project.
files
import
open
connect
run

You can run imported block programs even if you’re not signed up. This is a great way to try out Pybricks and see how it works.

Pairing with the Xbox Controller

The very first time, you’ll need to pair the hub with your controller:

  1. Turn the controller on.
  2. Press and hold the pairing button on the back.
  3. Release after a few seconds. The controller light will start flashing more rapidly. This is pairing mode.
  4. Then start your program. Either using the Pybricks app, or using the green button on the hub if you’ve already loaded the program earlier.

The hub will start looking for your controller when the program starts. When the connection is successful, the controller light will stop flashing and stay on for as long as the program is running.

You can see this in the video below.

Connecting without pairing

The next time you use it, pairing is not required. Just turn the controller on. The hub will connect to it automatically when your program runs.

The Xbox controller remembers only one device. If you use the controller with another LEGO hub or your Xbox console, and then with this hub again, you’ll need to pair them again as above.

Updating the Xbox Controller

If you often use the Xbox Controller with your console, it is probably already up to date. If you have not used it for a while or if you bought one recently, you may need to update it.

To update the controller without a console, you can use the Xbox Accessories app on a Windows computer. Connect the controller via USB to the computer and follow the instructions in the app to click on “Update now”. If it is already at firmware version 5.17 or later, you don’t need to do anything.

Technic Hub limitations

Once you start your program, the Technic Hub will disconnect from your computer to free up the connection for the Xbox Controller. This means you can’t use the Pybricks app to stop the program or change the code while the Xbox Controller is connected.

To change your program, stop the program by pressing the green button on the hub. Then you can connect to the hub with the Pybricks app again, and change your program as needed.

Running it as a Python program

You can also run this project as a Python (MicroPython) program. The following code was generated from the block program above. To run it, create a new empty Python program in Pybricks and copy the code into it.

from pybricks.iodevices import XboxController
from pybricks.parameters import Button, Direction, Port, Stop
from pybricks.pupdevices import Motor
from pybricks.tools import multitask, run_task, wait

# Set up all devices.
left = Motor(Port.A, Direction.COUNTERCLOCKWISE)
right = Motor(Port.B, Direction.CLOCKWISE)
function = Motor(Port.C, Direction.CLOCKWISE)
switch = Motor(Port.D, Direction.CLOCKWISE)
controller = XboxController()

# Initialize variables.
left_end = 0
right_end = 0
busy_switching = 1

async def switch_function(number):
    global busy_switching
    await wait(1)
    # This function takes care of moving the motor to positions 0, 1, 2, or 3:

    # 0: Blade up/down.
    # 1: Ripper.
    # 2: Ladder.
    # 3: Blade tilt.

    # First, indicate that we are going to be busy switching.
    busy_switching = 1
    for count in range(5):
        await wait(1)
        # Go to the target angle. But give up if it takes more
        # than 2 seconds, which means it's stuck for now.
        await multitask(switch.run_target(750, number * 90, Stop.COAST), wait(2000), race=True)
        # Let's check if we're on target.
        if abs(switch.angle() - number * 90) <= 10:
            # We are close to the target angle.
            # so we can exit this repeating loop.
            break
        # Otherwise, we're not done yet, so we must be stuck.
        # Let's wiggle the motor around to try to get it unstuck.
        switch.track_target(0)
        await wait(1000)
        switch.track_target(270)
        await wait(1000)
        switch.stop()
    # We're no longer busy switching, so we can drive again.
    busy_switching = 0

async def main1():
    # This main task will handle driving and the motor
    # that powers (not switches) the function gearbox.
    left.control.limits(acceleration=2500)
    right.control.limits(acceleration=2500)
    while True:
        await wait(1)
        if busy_switching:
            # If we are currently busy switching the function, we stop
            # driving and powering the function motor to be safe.
            left.stop()
            right.stop()
            function.stop()
        else:
            # Otherwise drive the motors based on the buttons that are pressed.
            # Use the bumpers for the function motor.
            if Button.RB in controller.buttons.pressed():
                function.dc(100)
            elif Button.LB in controller.buttons.pressed():
                function.dc(-100)
            else:
                function.stop()
            # Use the direction pad for driving.
            if controller.dpad() == 1:
                # Forward
                left.run(1000)
                right.run(1000)
            elif controller.dpad() == 2:
                # Forward / Right
                right.stop()
                left.run(1000)
            elif controller.dpad() == 3:
                # Right
                left.run(1000)
                right.run(-1000)
            elif controller.dpad() == 4:
                # Reverse / Right
                right.run(-1000)
                left.stop()
            elif controller.dpad() == 5:
                # Reverse
                left.run(-1000)
                right.run(-1000)
            elif controller.dpad() == 6:
                # Reverse / Left
                left.run(-1000)
                right.stop()
            elif controller.dpad() == 7:
                # Left
                left.run(-1000)
                right.run(1000)
            elif controller.dpad() == 8:
                # Forward / Left
                right.run(1000)
                left.stop()
            else:
                # Nothing, so stop.
                left.stop()
                right.stop()

async def main2():
    global right_end, left_end
    # This task will handle the motor that switches the function gearbox.
    # First it resets the gearbox by finding the start and end stops.
    await switch.run_until_stalled(500, Stop.COAST, 50)
    right_end = switch.angle()
    await switch.run_until_stalled(-500, Stop.COAST, 50)
    left_end = switch.angle()
    switch.reset_angle((left_end + 270 - right_end) / 2)
    await switch_function(0)
    # Now we run the main loop of operating the switch.
    while True:
        await wait(1)
        # Wait for the center switch to be pressed.
        while not any(controller.buttons.pressed()):
            await wait(1)
        # Then select the function based on which other button is pressed.
        if Button.X in controller.buttons.pressed():
            # Blade up/down function.
            await switch_function(0)
            while Button.X in controller.buttons.pressed():
                await wait(1)
        elif Button.A in controller.buttons.pressed():
            # Ripper function.
            await switch_function(1)
            while Button.A in controller.buttons.pressed():
                await wait(1)
        elif Button.Y in controller.buttons.pressed():
            # Ladder function.
            await switch_function(2)
            while Button.Y in controller.buttons.pressed():
                await wait(1)
        elif Button.B in controller.buttons.pressed():
            # Blade tilt function.
            await switch_function(3)
            while Button.B in controller.buttons.pressed():
                await wait(1)
        else:
            # No other button pressed, so don't switch.
            pass


async def main():
    await multitask(main1(), main2())

run_task(main())
Python representation of the block program.