Skip to content

MARLIN API​

To use the MARLIN API, import the marlin library in your Python code.

from marlin import Robot, IMU, RadioBeacon, Thruster, Servo, Phase

The API has four parts:

  1. Robot (configuration, match state)
  2. Controller (joysticks and buttons)
  3. Effectors (thrusters and servos)
  4. Sensors (IMU and radio beacon)

Robot​

Robot()​

Robot is a class. Import it from MARLIN and create one object robot at the start of your code.

Robot(endpoint: str | None = None)
from marlin import Robot

robot = Robot()

configure()​

configure() declares the type of hardware connected to each port to the Robot Brain. Only list the ports you use. A wrong declaration will throw an error.

robot.configure(
    *,
    sensors: dict[str, type[Sensor]],
    effectors: dict[str, type[Effector]],
) -> None
from marlin import Robot, IMU, Thruster, Servo

robot = Robot()
robot.configure(
    sensors={"S1": IMU},
    effectors={"M1": Thruster, "M8": Thruster, "M2": Servo},
)
Parameter Ports Types Limit
sensors S1–S6 IMU, RadioBeacon At most 2 IMUs, 1 RadioBeacon
effectors M1–M8 Thruster, Servo —
Error When
ValueError A bad port name, or too many sensors of one type
TypeError A type that is not a supported sensor or effector
RuntimeError Called twice, or the brain reports different hardware than you declared

run()​

run() watches robot.phase and calls the respective match phase function once.

You write one function per phase and pass them in here — each is optional, keyword-only, and receives the robot. Each function is called once, the moment its phase starts, so it must keep looping until the phase ends — that is what while robot.running: is for. run() blocks until the match is over or an E-Stop fires.

Whenever a phase ends — or your function returns early — MARLIN idles every effector. A function that returns early is not called again that phase; one that loops past the end is stopped and its commands ignored. An exception in your function stops the robot, then comes out of run() with the usual Python traceback.

robot.run(
    *,
    initialize: Callable[[Robot], None] | None = None,
    autonomous: Callable[[Robot], None] | None = None,
    driver: Callable[[Robot], None] | None = None,
) -> None
def autonomous(robot):
    while robot.running:
        ...

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

robot.run(autonomous=autonomous, driver=driver)
Parameter Description
initialize Runs before the first phase. Setup only: read sensors, print, work out numbers. Effector commands here are thrown away. Put anything that moves hardware in autonomous() or driver().
autonomous Runs when the Autonomous Period starts.
driver Runs when the Driver Controlled Period starts.

Match State​

Seven read-only properties describing the robot and the match. The Pool Server dictates the match phase. In its absence, MARLIN can specify the match phase from the control station.

Property Type Meaning
robot.running bool True while the phase your function was called for is still going
robot.phase Phase Which part of the match the robot is in right now
robot.link_ok bool True while the wireless link to the robot is healthy. False means your commands are not reaching the Robot.
robot.estopped bool True while the robot is emergency-stopped. E-Stop can be activated and cleared by the Pool Server and MARLIN control station.
robot.overtemp bool True while the brain is overheating. The Robot Brain's thermal protection trips at 50°C. Full functionality is restored once it has cooled to 45°C.
robot.battery_voltage float Battery's voltage in volts (V). Reads 0.0 before the Robot Brain reports anything.
robot.controller Controller The handheld control panel — see Controller

robot.running​

True while the phase your function was called for is still going. False when the phase ends, causing a while robot.running: loop to exit at the end of a match phase.

robot.running runs at a rate of 50 Hz, the same rate MARLIN sends commands. Adding time.sleep() decreases the frequency.

Example

def driver(robot):
    while robot.running:
        ...            # runs 50 times a second without time.sleep()

robot.phase​

Reads which part of the match the robot is in.

Example

from marlin import Phase

if robot.phase == Phase.AUTON:
    ...
Phase Meaning
CONFIGURING Program started, configure() not done yet
CONFIGURED Hardware declared and confirmed
AUTON Autonomous Period
PAUSE Between phases
DRIVER Driver Controlled Period
ESTOP Emergency Stop
POSTMATCH After the match

Controller​

The Controller has four joystick axes and ten independent buttons. Follow the naming conventions strictly. A wrong button or joystick axis name will throw an error.

Controller.

robot.controller.joystick()​

Returns one of the four joystick axes. There are two types of values that can be returned, scaled and raw values.

robot.controller.joystick(name: str) -> Joystick
Parameter Description
name "ONE", "TWO", "THREE" or "FOUR". Any other name raises ValueError.

robot.controller.joystick().value​

How far the stick is pushed: −100 to 100, and 0 when centred. The joystick axis range matches the thruster's duty, so a stick can drive a thruster directly.

power = robot.controller.joystick("ONE").value

robot.controller.joystick().raw​

The unscaled reading behind value: 0–4095, with centre at 2048. raw is used for checking a stick's true centre or writing your own scaling.

joystick_value = robot.controller.joystick("ONE").raw

robot.controller.button().is_down​

Returns the button state. True while the button is held down, False when released.

robot.controller.button(name: str).is_down -> bool

Reading the current state of the button:

if robot.controller.button("A").is_down:
    ...

Recording a button press event:

while robot.running:
  was_down = False
  down = robot.controller.button("A").is_down
  if down and not was_down:
      ...
  was_down = down
Parameter Description
name One of UP, DOWN, LEFT, RIGHT, X, Y, A, B, LEFT_TRIGGER, RIGHT_TRIGGER. An unknown name raises ValueError.

Effectors​

Declare the type of effector in configure() and fetch it with robot.thruster() or robot.servo(). These are the generic behaviours of effectors.

  1. An effector will follow the last command given in the current match phase.
  2. Effector inputs are clamped.
  3. Idle state differs by effector type.

robot.thruster().set_duty()​

Set the duty cycle of the thruster from a range of -100 to 100, with 0 being the idle state. Note that this does not represent the exact speed or power of the thruster.

robot.thruster(port: str).set_duty(duty: float) -> None
robot.thruster("M1").set_duty(100)
Parameter Description
duty −100 to 100, as a percentage of duty cycle. Values past either end are pinned to that end.

robot.servo().angle()​

Turns a positional servo to an angle from 0° to 180° and holds it there. Due to the nature of positional servos, it will always hold a position within its working range. The idle state of the servos is 90°. Keep that in mind when writing your code.

robot.servo(port: str).angle(deg: float) -> None
robot.servo("M2").angle(120)
Parameter Description
deg 0° to 180°. An angle past either end is pinned to that end.

robot.servo().set_speed()​

Set the movement speed of the servo. The speed range is 100 deg/s to 360 deg/s. If the speed isn't defined, the servo will default to 180 deg/s.

robot.servo(port: str).set_speed(speed: float) -> None
robot.servo("M2").set_speed(180)
Parameter Description
speed 100 deg/s to 360 deg/s. Default is 180 deg/s

Servo Overheating

Continuous driving of the servo for prolonged periods will cause it to overheat, potentially causing damage. Check on your servo periodically during heavy usage. If it feels warm, stop and let your servo cool down.

Sensors​

Like effectors, sensors are declared in configure() and fetched with robot.imu() or robot.radiobeacon(). Every sensor has one method: read().

read() returns the latest data measured by the sensor. Every sensor reading carries these three fields:

Field Meaning
timestamp Time in milliseconds when the Robot measures the reading. Robot Brain's clock starts from boot.
age How long ago it was measured, in milliseconds. The age is calculated based on the difference in time between the current and previous reading.
ok True if the reading exists and is less than 250 milliseconds old. Reject the reading if False.

robot.imu().read()​

IMU sensor: Acceleration, Turn Rate and Magnetic field.

robot.imu(port: str).read() -> ImuReading
reading = robot.imu("S1").read()
if reading.ok:
    print(reading)

Returns: ImuReading with ok, age, timestamp and:

Fields Unit Meaning
accel_x, accel_y, accel_z g Acceleration per axis; the axis pointing down reads ≈ -1g at rest
gyro_x, gyro_y, gyro_z °/s Turn rate per axis; 0 when not turning
mag_x, mag_y, mag_z — Magnetic field, raw uncalibrated integers

robot.radiobeacon().read()​

Long-range radio receiver. It reports how strong a beacon's signal is and which beacon sent it — not a direction or a distance, though strength does tend to rise as you get closer.

robot.radiobeacon(port: str).read() -> RadioBeaconReading
reading = robot.radiobeacon("S6").read()
if reading.ok:
    print(reading)

Returns: RadioBeaconReading with ok, age, timestamp and:

Field Unit Meaning
rssi dBm Signal strength, negative, and closer to zero is stronger.
snr dB How far the signal stands above the noise. Negative means buried.
beacon_id — Which beacon sent the message.
rx_count — Total beacon messages received since the start of the code.

Error Messages​

This page lists the errors raised by the MARLIN Python library. In the MARLIN Console they appear under the [OUT] tag, in two forms:

  1. Exceptions — the program stops with a Python traceback. If the last line of the traceback names one of the types below, MARLIN raised it and the tables explain the fix. ZeroDivisionError, NameError, SyntaxError, etc. are ordinary Python errors in your own code.
  2. [API] warnings — the program keeps running. MARLIN absorbed the mistake and printed what it did about it.

Messages tagged [EXT], [CON], or [POOL] come from the extension, the Controller, or the Pool Server, not from your code. See the MARLIN Console.

MARLIN idles every effector when a match phase ends, regardless of any error.

Error type Meaning Raised by
ValueError A value outside the accepted set configure(), joystick(), button()
TypeError The wrong kind of object in that place configure(), typed accessors
KeyError A port that was not declared Hardware accessors
LookupError Sensor absent or ambiguous with port omitted imu(), radiobeacon()
RuntimeError Calls in the wrong order, or the robot disagrees configure(), run()
LinkError The connection to the extension or robot failed Any call that talks to the robot
[API] warnings Non-fatal mistakes MARLIN worked around run(), effector commands

ValueError​

Message Cause and fix
Invalid sensor port 'S9'. Expected one of S1, S2, ... Sensor ports are S1–S6.
Invalid effector port 'M9'. Expected one of M1, M2, ... Effector ports are M1–M8.
At most 2 IMU may be configured; got 3 (S1, S2, S3). A robot supports at most 2 IMUs and 1 RadioBeacon. Remove the extras.
Unknown joystick 'FIVE'. Expected one of ONE, TWO, THREE, FOUR. Joysticks are named by the numbers printed on the panel.
Unknown button 'START'. Expected one of UP, DOWN, ... Buttons are named by their panel labels — the message lists all ten.

TypeError​

Message Cause and fix
S2: sensor declaration must be a Sensor subclass. Write the bare type in configure(): IMU, not IMU() and not "IMU".
S2: unsupported sensor type Servo. Known types: IMU, RadioBeacon. A sensor port received an effector type, or vice versa — sensors= and effectors= are swapped or mixed up.
M1 is configured as servo, not a Thruster. Wrong accessor for the port's type — this port holds a servo, so use robot.servo("M1").

KeyError​

Message Cause and fix
KeyError: 'M3' The port was not declared in configure(). Add it there, or use a port you did declare.

LookupError​

Message Cause and fix
No IMU is configured. robot.imu() was called with no IMU declared. Add one to sensors=.
2 IMUs are configured (S1, S2); specify the port, e.g. robot.imu('S1'). More than one sensor of that type is configured — name the port.

RuntimeError​

Message Cause and fix
Robot.configure() is only legal before the robot is armed. configure() runs once, at the top of the program. Remove the second call.
Robot.arm() requires a successful Robot.configure() first. run() was called before configure(). Swap them.
Robot configuration mismatch. Expected ..., got ... The declared hardware differs from what the Robot Brain reports. Ensure that your code is consistent with the wiring.

LinkError​

Raised when the connection between the program, the extension, and the robot fails. These are not code problems — check the control station: Controller connected, Robot Brain paired, battery in. ("C3" in these messages means the Controller.) LinkError is a subclass of RuntimeError.

Message Cause and fix
Extension not reachable at tcp://... The program was started outside MARLIN — VS Code's own Run button does this. Start it from Match Control instead.
Timed out waiting for the robot to confirm its configuration. Nothing answered: Controller not connected, or the Robot Brain unpaired or off.
Timed out waiting for arm acknowledgement ... As above, detected at the start of run().
The robot rejected its configuration. The Robot Brain refused the declared configuration.
The robot rejected arm. The Robot Brain refused to enable motors — it is e-stopped or overheated. Check the control station dials and E-Stop state.
Lost connection to the MARLIN extension ... The connection dropped mid-run — usually the Controller USB cable.

[API] warnings​

Printed to the MARLIN Console; the program keeps running. Each warning states what MARLIN did about the mistake.

Message Cause and fix
driver() has no loop. ... The function ran once and returned immediately. Loop on while robot.running:.
driver() loops but never checks robot.running, ... The loop cannot end on its own; MARLIN stops it at the phase change. Add robot.running to the loop condition.
the auton phase started, but run() was not given autonomous=; ... No function was passed for that phase. The robot idles — intended for a driver-only program.
driver() returned before the driver phase ended; ... The function finished early — often a return inside the loop. It is not called again that phase.
driver() ran past the end of the driver phase; MARLIN stopped it. — or ... is still running ...; its outputs are ignored. The phase ended while the code was still going. The overrunning code cannot move the robot.
M1: set_duty(nan) is not a usable number, ... A calculation produced nan — usually a division by zero. The effector keeps its previous setpoint.