Skip to content

Program structure

Every MARLIN program answers three questions: what hardware do I have, what should it do, and when should it do it. This page shows where each answer goes in the file, and why the shape has to be that way.

Most MARLIN programs look like this:

from marlin import Robot, Thruster

robot = Robot()

robot.configure(
    sensors={},
    effectors={"M1": Thruster},
)

def autonomous(robot):
    while robot.running:
        ...

def driver(robot):
    while robot.running:
        ...

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

This page explains the ideas behind that shape.

One Robot object

Create one Robot object near the top of your program:

robot = Robot()

You read all of these from the robot object:

  • configured sensors
  • configured effectors
  • controller input
  • battery voltage
  • E-Stop state
  • robot-link status
  • overtemp state.

Configuration happens once

Call configure() once before run():

robot.configure(
    sensors={"S1": IMU},
    effectors={"M1": Thruster, "M2": Servo},
)

Configuration tells MARLIN what hardware you expect to exist. MARLIN sends that configuration through the extension and checks the robot's response before your program can run. Nothing on the robot moves until the two match.

If configuration fails, do not catch the error and keep driving. Fix the declared ports or the physical robot configuration.

Callbacks describe match behaviour

robot.run() accepts callbacks: functions you define and MARLIN calls for you:

robot.run(
    initialize=initialize,
    autonomous=autonomous,
    driver=driver,
)
Callback Purpose
initialize(robot) Setup and checks before the match starts
autonomous(robot) Behaviour during the Autonomous Period
driver(robot) Behaviour during the Driver Controlled Period

You can omit callbacks you do not need.

initialize is for setup only: reading sensors, printing, and working out values the other callbacks need. Commanding a thruster or servo there does nothing, because the match has not started and MARLIN idles every output as soon as initialize returns. Anything that positions your robot goes at the top of autonomous or driver. See Match lifecycle.

There is no callback for the pause phase between the two periods, or for the phase after the match ends. MARLIN handles the robot on its own then: it idles every effector and keeps the connection alive, without running any of your code.

Use while robot.running

Phase callbacks usually need a while loop:

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

robot.running is true only while the phase that called your function is still going. When that phase ends, robot.running becomes false, the while loop stops on its own, and your function returns. You do not need to check the clock or break out of the loop yourself.

flowchart LR
    phaseStart["Driver Controlled Period starts"]
    invoke["MARLIN calls driver(robot)<br/>robot.running is now True"]
    looping["Your while loop repeats<br/>fifty times per second"]
    phaseEnd["Phase ends<br/>robot.running becomes False"]
    returned["Loop exits, your function returns<br/>MARLIN idles every effector"]

    phaseStart --> invoke --> looping --> phaseEnd --> returned
    looping --> looping

This is important: if your callback returns early, MARLIN idles all effector setpoints and does not call that same callback again during the same phase.

Reads are cached

Controller, sensor, and status reads return the latest cached value:

forward = robot.controller.joystick("ONE").value
reading = robot.imu("S1").read()
voltage = robot.battery_voltage

A read never waits. MARLIN keeps the most recent value it received from the robot, and hands you that value immediately. Your loop keeps running at full speed instead of pausing for the robot to answer.

For sensors, always check ok:

reading = robot.imu("S1").read()
if reading.ok:
    gyro_z = reading.gyro_z
else:
    robot.thruster("M1").set_duty(0)

Commands are remembered, not sent one by one

Calling an effector method does not send a message by itself. It records the value you want, and MARLIN keeps sending that value to the robot until you change it:

robot.thruster("M1").set_duty(25)
robot.servo("M2").angle(90)

Because the value is repeated for you, a thruster set to 25% stays at 25% even if your loop does not call set_duty() again. You never build or send messages yourself.

MARLIN idles every effector when a callback returns or when run() exits. Thrusters stop, and servos return to 90°.

Safety controls are outside your code

Your program can read robot.estopped, but it cannot trigger or clear E-Stop:

if robot.estopped:
    print("E-Stop is active")

E-Stop belongs to the MARLIN extension and the pool dashboard, so safety remains independent of your Python code.

The short version

create Robot
configure hardware
define callbacks
run match
MARLIN calls callbacks as the match moves on