Skip to content

Complete robot program

This page builds a competition-ready robot program one piece at a time: two thrusters, a servo, an IMU, and both match phases.

Everything below goes in one file: src/main.py in your MARLIN project. Each step adds to the file from the step before it. Every step runs on its own, so you can stop at any point, press Start Driver, and see what changed.

What you need

  • Python 3.10 or newer.
  • The MARLIN extension running in the editor. It bundles the MARLIN Python package, so there is nothing to install with pip.
  • A MARLIN project open in VS Code. A project is a folder containing src/main.py and a project.marlin marker file. The extension runs src/main.py, so there is no file to select.
  • A Controller connected through the extension when using real hardware.

The extension launches src/main.py with the MARLIN library and local link already configured. There is no need to copy the API into your project or open the Controller's serial port from Python.

If you have not written a MARLIN program at all yet, read Your first MARLIN program first. Step 1 below picks up exactly where it leaves off.

The ports

Every step names its ports as constants at the top of the file:

LEFT  = "M8"    # left thruster
RIGHT = "M1"    # right thruster
ARM   = "M2"    # servo
IMU_PORT = "S1" # IMU

Change these four values to match your robot and the rest of the code works unaltered. Sensor ports are S1S6; effector ports are M1M8.

1. One thruster

Start with the whole file. This is the program from Your first MARLIN program, without the empty autonomous stub. Autonomous arrives at step 5.

from marlin import Robot, Thruster

LEFT = "M8"

robot = Robot()
robot.configure(sensors={}, effectors={LEFT: Thruster})


def driver(robot):
    while robot.running:
        robot.thruster(LEFT).set_duty(robot.controller.joystick("ONE").value)


robot.run(driver=driver)

Joystick ONE is the left stick's up-and-down axis. .value gives -100 to 100. That is exactly the range set_duty() wants, so nothing needs converting.

You should see: pushing the left stick forward spins the thruster, pulling it back reverses it, and centring the stick stops it.

2. A second thruster

One thruster cannot turn. Add a second one, and give each stick a side of the boat to drive.

Change configure() to declare both ports:

LEFT  = "M8"
RIGHT = "M1"

robot = Robot()
robot.configure(sensors={}, effectors={LEFT: Thruster, RIGHT: Thruster})

Then replace driver with this:

def driver(robot):
    while robot.running:
        left  = robot.controller.joystick("ONE").value       # left stick, up/down
        right = robot.controller.joystick("THREE").value     # right stick, up/down

        robot.thruster(LEFT).set_duty(left)
        robot.thruster(RIGHT).set_duty(right)

This is tank drive: each stick owns one side of the boat, and nothing is mixed. A stick reads -100 to 100 and set_duty() takes -100 to 100, so each value goes straight through without any arithmetic of your own.

You should see: both sticks forward drives straight, both back reverses, and one forward while the other is pulled back spins the robot in place.

3. Stop the drift

A stick that does not sit perfectly centred reports a small value when nobody is touching it, and the robot creeps around the Pool between commands. Treat small readings as zero:

def driver(robot):
    while robot.running:
        left  = robot.controller.joystick("ONE").value
        right = robot.controller.joystick("THREE").value

        # abs() ignores the sign, so -3 and 3 both count as small.
        if abs(left) < 5:
            left = 0
        if abs(right) < 5:
            right = 0

        robot.thruster(LEFT).set_duty(left)
        robot.thruster(RIGHT).set_duty(right)

You should see: the robot holds still with both sticks released, and still responds normally as soon as you push past a light touch. If it creeps, raise the 5; if the sticks feel dead near the centre, lower it.

4. An arm on a button

Add a servo. Declare its port:

ARM = "M2"

robot.configure(
    sensors={},
    effectors={LEFT: Thruster, RIGHT: Thruster, ARM: Servo},
)

Add Servo to the import at the top of the file:

from marlin import Robot, Servo, Thruster

Then add press detection to driver:

def driver(robot):
    previous_a = False
    arm_is_up = False

    while robot.running:
        left  = robot.controller.joystick("ONE").value
        right = robot.controller.joystick("THREE").value

        if abs(left) < 5:
            left = 0
        if abs(right) < 5:
            right = 0

        robot.thruster(LEFT).set_duty(left)
        robot.thruster(RIGHT).set_duty(right)

        current_a = robot.controller.button("A").is_down

        # True only on the loop where the button went from up to down.
        if current_a and not previous_a:
            arm_is_up = not arm_is_up
            robot.servo(ARM).angle(90 if arm_is_up else 0)

        previous_a = current_a

is_down is a level, not an event. Without the previous_a comparison a single half-second press would toggle the arm dozens of times. arm_is_up remembers the position, because the servo cannot tell you where it is.

You should see: each press of A swings the arm once and it stays there. Holding A down does nothing more.

5. An autonomous routine

So far the robot only works during the Driver Controlled Period. Add an autonomous callback for the 20 seconds before it.

Timing needs Python's time module, so add it at the very top of the file:

import time

from marlin import Robot, Servo, Thruster

Then add this function, and pass it to run():

def autonomous(robot):
    robot.servo(ARM).angle(0)          # stow the arm before moving
    turn_start = time.monotonic()

    while robot.running:
        if time.monotonic() - turn_start < 2.0:
            robot.thruster(LEFT).set_duty(30)
            robot.thruster(RIGHT).set_duty(-30)
        else:
            robot.thruster(LEFT).set_duty(0)
            robot.thruster(RIGHT).set_duty(0)


robot.run(autonomous=autonomous, driver=driver)

time.monotonic() returns seconds as a decimal. Take one reading before the loop and subtract it inside the loop to get elapsed time. Do not use time.sleep() to time the movement. While your code sleeps it cannot read anything or change what the robot is doing.

The Autonomous Period is 20 seconds, so a routine whose deadlines add up past 20 never finishes. In a Qualification Match your partner's robot is also in the Pool, running its own routine at the same time. See Alliance.

You should see: on Start Auton, the arm stows, the robot spins in place for two seconds, then holds still until the period ends.

stateDiagram-v2
    [*] --> Configured: robot.configure()
    Configured --> Auton: Autonomous Period starts
    Auton --> Pause: Autonomous Period ends
    Pause --> Driver: Driver Controlled Period starts
    Driver --> MatchOver: Driver Controlled Period ends
    Auton --> Estop: E-Stop
    Driver --> Estop: E-Stop
    Pause --> Estop: E-Stop
    Estop --> [*]
    MatchOver --> [*]

6. Check the sensor before trusting it

Add an IMU, and make autonomous refuse to drive on stale data.

IMU_PORT = "S1"

robot.configure(
    sensors={IMU_PORT: IMU},
    effectors={LEFT: Thruster, RIGHT: Thruster, ARM: Servo},
)

Add IMU to the import, then guard the loop:

def autonomous(robot):
    robot.servo(ARM).angle(0)
    turn_start = time.monotonic()

    while robot.running:
        reading = robot.imu(IMU_PORT).read()

        if not reading.ok:
            # No fresh reading: hold still rather than drive blind.
            robot.thruster(LEFT).set_duty(0)
            robot.thruster(RIGHT).set_duty(0)
            continue

        if time.monotonic() - turn_start < 2.0:
            robot.thruster(LEFT).set_duty(30)
            robot.thruster(RIGHT).set_duty(-30)
        else:
            robot.thruster(LEFT).set_duty(0)
            robot.thruster(RIGHT).set_duty(0)

Always check a reading's ok field before using it to decide how to move. The turn here is still timed, not driven to an angle, because MARLIN's IMU has no heading or compass field. See Gyro turn estimate if you want to estimate an angle from gyro_z.

You should see: no change while the IMU is healthy. Unplug it and the robot holds still through autonomous instead of turning.

7. Check the robot before the match

initialize runs once before the match starts. Use it for setup and checks:

def initialize(robot):
    print("Battery:", robot.battery_voltage, "V")


robot.run(
    initialize=initialize,
    autonomous=autonomous,
    driver=driver,
)

initialize is for setup only. Commanding a thruster or servo there does nothing, because MARLIN idles every output as soon as initialize returns. That is why the arm is stowed at the top of autonomous instead. See Match lifecycle.

You should see: a line like battery 12.4 V in the MARLIN Console before the match starts.

Complete program

All seven steps together:

import time

from marlin import IMU, Robot, Servo, Thruster

LEFT  = "M8"
RIGHT = "M1"
ARM   = "M2"
IMU_PORT = "S1"

robot = Robot()
robot.configure(
    sensors={IMU_PORT: IMU},
    effectors={LEFT: Thruster, RIGHT: Thruster, ARM: Servo},
)


def initialize(robot):
    print("Battery:", robot.battery_voltage, "V")


def autonomous(robot):
    robot.servo(ARM).angle(0)          # stow the arm before moving
    turn_start = time.monotonic()

    while robot.running:
        reading = robot.imu(IMU_PORT).read()

        if not reading.ok:
            robot.thruster(LEFT).set_duty(0)
            robot.thruster(RIGHT).set_duty(0)
            continue

        if time.monotonic() - turn_start < 2.0:
            robot.thruster(LEFT).set_duty(30)
            robot.thruster(RIGHT).set_duty(-30)
        else:
            robot.thruster(LEFT).set_duty(0)
            robot.thruster(RIGHT).set_duty(0)


def driver(robot):
    previous_a = False
    arm_is_up = False

    while robot.running:
        left  = robot.controller.joystick("ONE").value       # left stick, up/down
        right = robot.controller.joystick("THREE").value     # right stick, up/down

        if abs(left) < 5:
            left = 0
        if abs(right) < 5:
            right = 0

        robot.thruster(LEFT).set_duty(left)
        robot.thruster(RIGHT).set_duty(right)

        current_a = robot.controller.button("A").is_down
        if current_a and not previous_a:
            arm_is_up = not arm_is_up
            robot.servo(ARM).angle(90 if arm_is_up else 0)
        previous_a = current_a


robot.run(
    initialize=initialize,
    autonomous=autonomous,
    driver=driver,
)

Examples has smaller patterns you can drop into this file: trigger and D-pad control, a multi-step auton, and beacon seeking. It also shows how to combine two of them.

For the rules behind what you just built, read Hardware configuration and Match lifecycle.